tailwind-lint 0.7.0 → 0.9.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 +47 -0
- package/dist/cli.cjs +130 -50
- package/dist/linter.cjs +31 -8
- package/dist/linter.d.cts +5 -1
- package/dist/{state-BHl8x2Q1.cjs → state-CeRvUDZb.cjs} +64 -11
- package/package.json +80 -76
package/README.md
CHANGED
|
@@ -65,6 +65,7 @@ tailwind-lint --verbose
|
|
|
65
65
|
- `-c, --config <path>` - Path to Tailwind config file (default: auto-discover)
|
|
66
66
|
- `-a, --auto` - Auto-discover files from config content patterns (legacy, enabled by default)
|
|
67
67
|
- `--fix` - Automatically fix problems that can be fixed
|
|
68
|
+
- `--format <text|json>` - Output format (`text` default, `json` for machine-readable output)
|
|
68
69
|
- `-v, --verbose` - Enable verbose logging for debugging
|
|
69
70
|
- `-h, --help` - Show help message
|
|
70
71
|
- `--version` - Show version number
|
|
@@ -83,8 +84,33 @@ tailwind-lint "**/*.vue" --fix
|
|
|
83
84
|
|
|
84
85
|
# Lint with a specific CSS config (v4)
|
|
85
86
|
tailwind-lint --config ./styles/app.css
|
|
87
|
+
|
|
88
|
+
# Machine-readable output for LLMs/agents
|
|
89
|
+
tailwind-lint --auto --format json
|
|
86
90
|
```
|
|
87
91
|
|
|
92
|
+
## LLM / Agent Integration
|
|
93
|
+
|
|
94
|
+
Use JSON output to avoid brittle text parsing:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
tailwind-lint --auto --format json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The JSON payload includes:
|
|
101
|
+
|
|
102
|
+
- `ok` - `true` when no errors are found
|
|
103
|
+
- `summary` - counts for `errors`, `warnings`, `fixed`, `filesWithIssues`, `totalFilesProcessed`
|
|
104
|
+
- `config` - resolved runtime values (`cwd`, `configPath`, `autoDiscover`, `fix`, `patterns`)
|
|
105
|
+
- `files[]` - per-file diagnostics with 1-based `line`/`column` ranges
|
|
106
|
+
|
|
107
|
+
Typical agent flow:
|
|
108
|
+
|
|
109
|
+
1. Run `tailwind-lint --auto --format json`.
|
|
110
|
+
2. If `summary.errors > 0`, fail the check and surface diagnostics.
|
|
111
|
+
3. If only warnings exist, optionally continue and open a cleanup task.
|
|
112
|
+
4. Re-run with `--fix` when autofix is allowed.
|
|
113
|
+
|
|
88
114
|
## Configuration
|
|
89
115
|
|
|
90
116
|
### Tailwind CSS v4
|
|
@@ -172,4 +198,25 @@ pnpm format
|
|
|
172
198
|
|
|
173
199
|
# Check code without fixing
|
|
174
200
|
pnpm lint
|
|
201
|
+
|
|
202
|
+
# Preview next version locally (no publish)
|
|
203
|
+
pnpm release:dry
|
|
175
204
|
```
|
|
205
|
+
|
|
206
|
+
## Releases
|
|
207
|
+
|
|
208
|
+
Releases are automated with Semantic Release on pushes to `main`.
|
|
209
|
+
|
|
210
|
+
- Version bump is derived from Conventional Commits.
|
|
211
|
+
- npm release and GitHub release are generated automatically.
|
|
212
|
+
- npm publish uses npm Trusted Publishing (OIDC), no `NPM_TOKEN` required.
|
|
213
|
+
|
|
214
|
+
Commit examples:
|
|
215
|
+
|
|
216
|
+
- `feat: add json output mode` -> minor release
|
|
217
|
+
- `fix: resolve v4 config discovery in monorepos` -> patch release
|
|
218
|
+
- `feat: drop Node 20 support` + commit body `BREAKING CHANGE: Node 20 is no longer supported` -> major release
|
|
219
|
+
- `perf: speed up config discovery` -> patch release
|
|
220
|
+
- `docs: update readme` -> no release
|
|
221
|
+
|
|
222
|
+
`pnpm release:dry` runs against the local repo metadata (`--repository-url .`) so it does not require GitHub remote access.
|
package/dist/cli.cjs
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_state = require('./state-
|
|
2
|
+
const require_state = require('./state-CeRvUDZb.cjs');
|
|
3
3
|
const require_linter = require('./linter.cjs');
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
node_path = require_state.__toESM(node_path);
|
|
6
|
-
let
|
|
7
|
-
|
|
6
|
+
let ansis = require("ansis");
|
|
7
|
+
ansis = require_state.__toESM(ansis);
|
|
8
8
|
let node_fs = require("node:fs");
|
|
9
9
|
node_fs = require_state.__toESM(node_fs);
|
|
10
10
|
let commander = require("commander");
|
|
11
11
|
|
|
12
|
-
//#region src/
|
|
13
|
-
|
|
14
|
-
function countDiagnosticsBySeverity(diagnostics) {
|
|
12
|
+
//#region src/output.ts
|
|
13
|
+
function countBySeverity(diagnostics) {
|
|
15
14
|
let errors = 0;
|
|
16
15
|
let warnings = 0;
|
|
17
16
|
for (const diagnostic of diagnostics) {
|
|
@@ -23,6 +22,64 @@ function countDiagnosticsBySeverity(diagnostics) {
|
|
|
23
22
|
warnings
|
|
24
23
|
};
|
|
25
24
|
}
|
|
25
|
+
function toJsonSeverity(severity) {
|
|
26
|
+
if (severity === require_state.SEVERITY.ERROR) return "error";
|
|
27
|
+
if (severity === require_state.SEVERITY.WARNING) return "warning";
|
|
28
|
+
return "info";
|
|
29
|
+
}
|
|
30
|
+
function toJsonDiagnostic(diagnostic) {
|
|
31
|
+
return {
|
|
32
|
+
line: diagnostic.range.start.line + 1,
|
|
33
|
+
column: diagnostic.range.start.character + 1,
|
|
34
|
+
endLine: diagnostic.range.end.line + 1,
|
|
35
|
+
endColumn: diagnostic.range.end.character + 1,
|
|
36
|
+
severity: toJsonSeverity(diagnostic.severity),
|
|
37
|
+
code: diagnostic.code ?? null,
|
|
38
|
+
message: diagnostic.message,
|
|
39
|
+
source: diagnostic.source
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function createJsonOutput({ files, totalFilesProcessed, cwd, configPath, autoDiscover, fix, patterns }) {
|
|
43
|
+
let errors = 0;
|
|
44
|
+
let warnings = 0;
|
|
45
|
+
let fixed = 0;
|
|
46
|
+
let filesWithIssues = 0;
|
|
47
|
+
const mappedFiles = files.map((file) => {
|
|
48
|
+
const severityCount = countBySeverity(file.diagnostics);
|
|
49
|
+
errors += severityCount.errors;
|
|
50
|
+
warnings += severityCount.warnings;
|
|
51
|
+
fixed += file.fixedCount || 0;
|
|
52
|
+
if (file.diagnostics.length > 0) filesWithIssues++;
|
|
53
|
+
return {
|
|
54
|
+
path: file.path,
|
|
55
|
+
fixed: file.fixed || false,
|
|
56
|
+
fixedCount: file.fixedCount || 0,
|
|
57
|
+
diagnostics: file.diagnostics.map(toJsonDiagnostic)
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
ok: errors === 0,
|
|
62
|
+
summary: {
|
|
63
|
+
errors,
|
|
64
|
+
warnings,
|
|
65
|
+
fixed,
|
|
66
|
+
filesWithIssues,
|
|
67
|
+
totalFilesProcessed
|
|
68
|
+
},
|
|
69
|
+
config: {
|
|
70
|
+
cwd,
|
|
71
|
+
configPath: configPath || null,
|
|
72
|
+
autoDiscover,
|
|
73
|
+
fix,
|
|
74
|
+
patterns
|
|
75
|
+
},
|
|
76
|
+
files: mappedFiles
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/cli.ts
|
|
82
|
+
const MAX_FILENAME_DISPLAY_LENGTH = 50;
|
|
26
83
|
function resolveOptions(files, options) {
|
|
27
84
|
const hasConfigFlag = !!options.config;
|
|
28
85
|
const hasAutoFlag = !!options.auto;
|
|
@@ -60,13 +117,13 @@ async function displayResults(files, fixMode) {
|
|
|
60
117
|
console.log();
|
|
61
118
|
isFirstFile = false;
|
|
62
119
|
} else console.log();
|
|
63
|
-
console.log(
|
|
120
|
+
console.log(ansis.default.underline.bold(file.path));
|
|
64
121
|
if (fixMode && file.fixed) {
|
|
65
122
|
const issueText = `${file.fixedCount || 0} issue${file.fixedCount !== 1 ? "s" : ""}`;
|
|
66
|
-
console.log(
|
|
123
|
+
console.log(ansis.default.green(` ✔ Fixed ${issueText}`));
|
|
67
124
|
totalFixed += file.fixedCount || 0;
|
|
68
125
|
}
|
|
69
|
-
const { errors, warnings } =
|
|
126
|
+
const { errors, warnings } = countBySeverity(file.diagnostics);
|
|
70
127
|
totalErrors += errors;
|
|
71
128
|
totalWarnings += warnings;
|
|
72
129
|
if (file.diagnostics.length > 0) filesWithIssues++;
|
|
@@ -74,16 +131,16 @@ async function displayResults(files, fixMode) {
|
|
|
74
131
|
const line = diagnostic.range.start.line + 1;
|
|
75
132
|
const char = diagnostic.range.start.character + 1;
|
|
76
133
|
const severity = diagnostic.severity === require_state.SEVERITY.ERROR ? "error" : "warning";
|
|
77
|
-
const severityColor = diagnostic.severity === require_state.SEVERITY.ERROR ?
|
|
78
|
-
const code = diagnostic.code ?
|
|
79
|
-
console.log(` ${
|
|
134
|
+
const severityColor = diagnostic.severity === require_state.SEVERITY.ERROR ? ansis.default.red(severity) : ansis.default.yellow(severity);
|
|
135
|
+
const code = diagnostic.code ? ansis.default.dim(` (${diagnostic.code})`) : "";
|
|
136
|
+
console.log(` ${ansis.default.dim(`${line}:${char}`)} ${severityColor} ${diagnostic.message}${code}`);
|
|
80
137
|
}
|
|
81
138
|
}
|
|
82
139
|
console.log();
|
|
83
140
|
if (totalErrors === 0 && totalWarnings === 0) if (totalFixed > 0) {
|
|
84
141
|
const issueText = `${totalFixed} issue${totalFixed !== 1 ? "s" : ""}`;
|
|
85
|
-
console.log(
|
|
86
|
-
} else console.log(
|
|
142
|
+
console.log(ansis.default.green.bold(`✔ Fixed ${issueText}`));
|
|
143
|
+
} else console.log(ansis.default.green.bold("✔ No issues found"));
|
|
87
144
|
else {
|
|
88
145
|
const parts = [];
|
|
89
146
|
if (totalErrors > 0) parts.push(`${totalErrors} error${totalErrors !== 1 ? "s" : ""}`);
|
|
@@ -92,7 +149,7 @@ async function displayResults(files, fixMode) {
|
|
|
92
149
|
const summary = `Found ${parts.join(" and ")} in ${fileText}`;
|
|
93
150
|
if (totalFixed > 0) {
|
|
94
151
|
const issueText = `${totalFixed} issue${totalFixed !== 1 ? "s" : ""}`;
|
|
95
|
-
console.log(
|
|
152
|
+
console.log(ansis.default.green.bold(`✔ Fixed ${issueText}`));
|
|
96
153
|
console.log(summary);
|
|
97
154
|
} else console.log(summary);
|
|
98
155
|
}
|
|
@@ -109,85 +166,108 @@ const getVersion = () => {
|
|
|
109
166
|
program.configureHelp({ formatHelp: (cmd, helper) => {
|
|
110
167
|
const termWidth = helper.padWidth(cmd, helper);
|
|
111
168
|
let output = "";
|
|
112
|
-
output += `${
|
|
169
|
+
output += `${ansis.default.bold.cyan("Usage:")} ${helper.commandUsage(cmd)}\n\n`;
|
|
113
170
|
if (cmd.description()) output += `${cmd.description()}\n\n`;
|
|
114
171
|
const args = helper.visibleArguments(cmd);
|
|
115
172
|
if (args.length > 0) {
|
|
116
|
-
output += `${
|
|
173
|
+
output += `${ansis.default.bold.cyan("Arguments:")}\n`;
|
|
117
174
|
args.forEach((arg) => {
|
|
118
175
|
const argName = arg.required ? `<${arg.name()}>` : `[${arg.name()}]`;
|
|
119
|
-
output += ` ${
|
|
176
|
+
output += ` ${ansis.default.green(argName.padEnd(termWidth))} ${arg.description}\n`;
|
|
120
177
|
});
|
|
121
178
|
output += "\n";
|
|
122
179
|
}
|
|
123
180
|
const options = helper.visibleOptions(cmd);
|
|
124
181
|
if (options.length > 0) {
|
|
125
|
-
output += `${
|
|
182
|
+
output += `${ansis.default.bold.cyan("Options:")}\n`;
|
|
126
183
|
options.forEach((option) => {
|
|
127
184
|
const flags = helper.optionTerm(option);
|
|
128
185
|
const description = helper.optionDescription(option);
|
|
129
|
-
output += ` ${
|
|
186
|
+
output += ` ${ansis.default.yellow(flags.padEnd(termWidth))} ${description}\n`;
|
|
130
187
|
});
|
|
131
188
|
output += "\n";
|
|
132
189
|
}
|
|
133
190
|
return output;
|
|
134
191
|
} });
|
|
135
|
-
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 content patterns").option("--fix", "Automatically fix problems that can be fixed").option("-v, --verbose", "Enable verbose logging for debugging").addHelpText("after", `
|
|
136
|
-
${
|
|
137
|
-
${
|
|
138
|
-
${
|
|
139
|
-
${
|
|
140
|
-
${
|
|
141
|
-
${
|
|
192
|
+
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 content 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", `
|
|
193
|
+
${ansis.default.bold.cyan("Examples:")}
|
|
194
|
+
${ansis.default.dim("$")} tailwind-lint
|
|
195
|
+
${ansis.default.dim("$")} tailwind-lint ${ansis.default.green("\"src/**/*.{js,jsx,ts,tsx}\"")}
|
|
196
|
+
${ansis.default.dim("$")} tailwind-lint ${ansis.default.yellow("--config")} ${ansis.default.green("./tailwind.config.js")}
|
|
197
|
+
${ansis.default.dim("$")} tailwind-lint ${ansis.default.green("\"src/**/*.tsx\"")} ${ansis.default.yellow("--fix")}
|
|
198
|
+
${ansis.default.dim("$")} tailwind-lint ${ansis.default.green("\"**/*.vue\"")}
|
|
142
199
|
|
|
143
|
-
${
|
|
144
|
-
${
|
|
145
|
-
${
|
|
146
|
-
${
|
|
147
|
-
${
|
|
148
|
-
${
|
|
200
|
+
${ansis.default.bold.cyan("Notes:")}
|
|
201
|
+
${ansis.default.dim("•")} Running without arguments auto-discovers config and files
|
|
202
|
+
${ansis.default.dim("•")} For v3: uses content patterns from tailwind.config.js
|
|
203
|
+
${ansis.default.dim("•")} For v4: uses @source patterns from CSS config, or default pattern
|
|
204
|
+
${ansis.default.dim("•")} Default pattern: ${ansis.default.dim("./**/*.{js,jsx,ts,tsx,html}")}
|
|
205
|
+
${ansis.default.dim("•")} Use ${ansis.default.yellow("--fix")} to automatically resolve fixable issues
|
|
149
206
|
`).action(async (files, options) => {
|
|
150
207
|
const hasConfigFlag = !!options.config;
|
|
151
208
|
const hasAutoFlag = !!options.auto;
|
|
152
209
|
if (!(files.length > 0) && !hasAutoFlag && !hasConfigFlag) options.auto = true;
|
|
153
210
|
const resolved = resolveOptions(files, options);
|
|
211
|
+
const isJsonOutput = (options.format === "json" ? "json" : "text") === "json";
|
|
154
212
|
try {
|
|
155
|
-
if (resolved.verbose) {
|
|
156
|
-
console.log(
|
|
157
|
-
console.log(
|
|
158
|
-
console.log(
|
|
159
|
-
console.log(
|
|
160
|
-
console.log(
|
|
213
|
+
if (resolved.verbose && !isJsonOutput) {
|
|
214
|
+
console.log(ansis.default.cyan.bold("→ Configuration"));
|
|
215
|
+
console.log(ansis.default.dim(` Working directory: ${resolved.cwd}`));
|
|
216
|
+
console.log(ansis.default.dim(` Config path: ${resolved.configPath || "auto-discover"}`));
|
|
217
|
+
console.log(ansis.default.dim(` Fix mode: ${resolved.fix}`));
|
|
218
|
+
console.log(ansis.default.dim(` Patterns: ${resolved.patterns.length > 0 ? resolved.patterns.join(", ") : "auto-discover"}`));
|
|
161
219
|
console.log();
|
|
162
220
|
}
|
|
163
221
|
const results = await require_linter.lint({
|
|
164
222
|
...resolved,
|
|
165
223
|
onProgress: (current, total, file) => {
|
|
224
|
+
if (isJsonOutput) return;
|
|
166
225
|
if (process.stdout.isTTY && !resolved.verbose) {
|
|
167
226
|
const displayFile = truncateFilename(file);
|
|
168
|
-
process.stdout.write(`\r${
|
|
169
|
-
} else if (resolved.verbose) console.log(
|
|
227
|
+
process.stdout.write(`\r${ansis.default.cyan("→")} Linting files... ${ansis.default.dim(`(${current}/${total})`)} ${ansis.default.dim(displayFile)}${" ".repeat(require_state.TERMINAL_PADDING)}`);
|
|
228
|
+
} else if (resolved.verbose) console.log(ansis.default.dim(` [${current}/${total}] Linting ${file}`));
|
|
170
229
|
}
|
|
171
230
|
});
|
|
172
|
-
if (process.stdout.isTTY && !resolved.verbose) process.stdout.write(`\r${" ".repeat(require_state.TERMINAL_WIDTH)}\r`);
|
|
231
|
+
if (process.stdout.isTTY && !resolved.verbose && !isJsonOutput) process.stdout.write(`\r${" ".repeat(require_state.TERMINAL_WIDTH)}\r`);
|
|
173
232
|
if (results.totalFilesProcessed === 0) {
|
|
174
|
-
console.log(
|
|
175
|
-
|
|
233
|
+
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
234
|
+
...resolved,
|
|
235
|
+
files: [],
|
|
236
|
+
totalFilesProcessed: 0
|
|
237
|
+
})));
|
|
238
|
+
else {
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(ansis.default.yellow("⚠ No files found to lint"));
|
|
241
|
+
}
|
|
176
242
|
process.exit(0);
|
|
177
243
|
}
|
|
178
244
|
if (results.files.length === 0) {
|
|
179
|
-
console.log(
|
|
245
|
+
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
246
|
+
...resolved,
|
|
247
|
+
files: [],
|
|
248
|
+
totalFilesProcessed: results.totalFilesProcessed
|
|
249
|
+
})));
|
|
250
|
+
else console.log(ansis.default.green.bold("✔ No issues found"));
|
|
180
251
|
process.exit(0);
|
|
181
252
|
}
|
|
182
|
-
|
|
253
|
+
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
254
|
+
...resolved,
|
|
255
|
+
files: results.files,
|
|
256
|
+
totalFilesProcessed: results.totalFilesProcessed
|
|
257
|
+
})));
|
|
258
|
+
else await displayResults(results.files, resolved.fix);
|
|
183
259
|
const hasErrors = results.files.some((file) => file.diagnostics.some((d) => d.severity === require_state.SEVERITY.ERROR));
|
|
184
260
|
process.exit(hasErrors ? 1 : 0);
|
|
185
261
|
} catch (error) {
|
|
186
262
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
187
|
-
console.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
263
|
+
if (isJsonOutput) console.log(JSON.stringify({
|
|
264
|
+
ok: false,
|
|
265
|
+
error: errorMessage
|
|
266
|
+
}));
|
|
267
|
+
else console.error(ansis.default.red("✖ Error:"), errorMessage);
|
|
268
|
+
if (resolved.verbose && error instanceof Error && !isJsonOutput) {
|
|
269
|
+
console.error(ansis.default.dim("\nStack trace:"));
|
|
270
|
+
console.error(ansis.default.dim(error.stack || error.toString()));
|
|
191
271
|
}
|
|
192
272
|
process.exit(1);
|
|
193
273
|
}
|
package/dist/linter.cjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_state = require('./state-
|
|
2
|
+
const require_state = require('./state-CeRvUDZb.cjs');
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
node_path = require_state.__toESM(node_path);
|
|
5
5
|
let _tailwindcss_language_service = require("@tailwindcss/language-service");
|
|
6
|
-
let
|
|
7
|
-
|
|
8
|
-
let
|
|
9
|
-
fast_glob = require_state.__toESM(fast_glob);
|
|
6
|
+
let ansis = require("ansis");
|
|
7
|
+
ansis = require_state.__toESM(ansis);
|
|
8
|
+
let tinyglobby = require("tinyglobby");
|
|
10
9
|
let vscode_languageserver_textdocument = require("vscode-languageserver-textdocument");
|
|
11
10
|
|
|
12
11
|
//#region src/linter.ts
|
|
@@ -33,11 +32,12 @@ async function discoverFiles(cwd, patterns, configPath, autoDiscover) {
|
|
|
33
32
|
return expandPatterns(cwd, patterns);
|
|
34
33
|
}
|
|
35
34
|
async function expandPatterns(cwd, patterns, extraIgnore = []) {
|
|
36
|
-
|
|
35
|
+
const files = await (0, tinyglobby.glob)(patterns, {
|
|
37
36
|
cwd,
|
|
38
37
|
absolute: false,
|
|
39
38
|
ignore: [...require_state.DEFAULT_IGNORE_PATTERNS, ...extraIgnore]
|
|
40
39
|
});
|
|
40
|
+
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
41
41
|
}
|
|
42
42
|
async function discoverFilesFromConfig(cwd, configPath) {
|
|
43
43
|
const configFilePath = await require_state.findTailwindConfigPath(cwd, configPath);
|
|
@@ -50,7 +50,9 @@ async function discoverFilesFromConfig(cwd, configPath) {
|
|
|
50
50
|
return expandPatterns(cwd, patterns);
|
|
51
51
|
}
|
|
52
52
|
const configDir = node_path.dirname(configFilePath);
|
|
53
|
-
const
|
|
53
|
+
const cssContent = require_state.readFileSync(configFilePath);
|
|
54
|
+
const { include, exclude } = extractSourcePatterns(cssContent);
|
|
55
|
+
const importSource = extractImportSourceDirectives(cssContent);
|
|
54
56
|
const resolveFromConfig = (pattern) => {
|
|
55
57
|
const absolutePattern = node_path.resolve(configDir, pattern);
|
|
56
58
|
return node_path.relative(cwd, absolutePattern);
|
|
@@ -59,6 +61,8 @@ async function discoverFilesFromConfig(cwd, configPath) {
|
|
|
59
61
|
const gitignorePatterns = require_state.readGitignorePatterns(cwd);
|
|
60
62
|
const extraIgnore = [...resolvedExclude, ...gitignorePatterns];
|
|
61
63
|
if (include.length > 0) return expandPatterns(cwd, include.map(resolveFromConfig), extraIgnore);
|
|
64
|
+
if (importSource.roots.length > 0) return expandPatterns(cwd, importSource.roots.map((root) => resolveFromConfig(node_path.join(root, "**/*.{js,jsx,ts,tsx,html,vue,svelte,astro,mdx}"))), extraIgnore);
|
|
65
|
+
if (importSource.disableAutoSource) return [];
|
|
62
66
|
return expandPatterns(cwd, [require_state.DEFAULT_FILE_PATTERN], extraIgnore);
|
|
63
67
|
}
|
|
64
68
|
function extractContentPatterns(config) {
|
|
@@ -80,6 +84,24 @@ function extractSourcePatterns(cssContent) {
|
|
|
80
84
|
exclude
|
|
81
85
|
};
|
|
82
86
|
}
|
|
87
|
+
function extractImportSourceDirectives(cssContent) {
|
|
88
|
+
const roots = [];
|
|
89
|
+
let disableAutoSource = false;
|
|
90
|
+
for (const match of cssContent.matchAll(/@import\s+["']tailwindcss(?:[^;]*?)\ssource\(\s*(none|["'][^"']+["'])\s*\)/g)) {
|
|
91
|
+
const raw = match[1];
|
|
92
|
+
if (!raw) continue;
|
|
93
|
+
if (raw === "none") {
|
|
94
|
+
disableAutoSource = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const sourceRoot = raw.slice(1, -1).trim();
|
|
98
|
+
if (sourceRoot.length > 0) roots.push(sourceRoot);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
roots: [...new Set(roots)],
|
|
102
|
+
disableAutoSource
|
|
103
|
+
};
|
|
104
|
+
}
|
|
83
105
|
async function processFiles(state, cwd, files, fix, onProgress) {
|
|
84
106
|
const results = [];
|
|
85
107
|
for (let i = 0; i < files.length; i += require_state.CONCURRENT_FILES) {
|
|
@@ -127,7 +149,7 @@ async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verb
|
|
|
127
149
|
const state = await initializeState(cwd, configPath, verbose);
|
|
128
150
|
const files = await discoverFiles(cwd, patterns, configPath, autoDiscover);
|
|
129
151
|
if (verbose) {
|
|
130
|
-
console.log(
|
|
152
|
+
console.log(ansis.default.cyan.bold(`→ Discovered ${files.length} file${files.length !== 1 ? "s" : ""} to lint`));
|
|
131
153
|
console.log();
|
|
132
154
|
}
|
|
133
155
|
if (files.length === 0) return {
|
|
@@ -141,5 +163,6 @@ async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verb
|
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
//#endregion
|
|
166
|
+
exports.extractImportSourceDirectives = extractImportSourceDirectives;
|
|
144
167
|
exports.extractSourcePatterns = extractSourcePatterns;
|
|
145
168
|
exports.lint = lint;
|
package/dist/linter.d.cts
CHANGED
|
@@ -26,6 +26,10 @@ declare function extractSourcePatterns(cssContent: string): {
|
|
|
26
26
|
include: string[];
|
|
27
27
|
exclude: string[];
|
|
28
28
|
};
|
|
29
|
+
declare function extractImportSourceDirectives(cssContent: string): {
|
|
30
|
+
roots: string[];
|
|
31
|
+
disableAutoSource: boolean;
|
|
32
|
+
};
|
|
29
33
|
declare function lint({
|
|
30
34
|
cwd,
|
|
31
35
|
patterns,
|
|
@@ -36,4 +40,4 @@ declare function lint({
|
|
|
36
40
|
onProgress
|
|
37
41
|
}: LintOptions): Promise<LintResult>;
|
|
38
42
|
//#endregion
|
|
39
|
-
export { type LintFileResult, type LintOptions, type LintResult, extractSourcePatterns, lint };
|
|
43
|
+
export { type LintFileResult, type LintOptions, type LintResult, extractImportSourceDirectives, extractSourcePatterns, lint };
|
|
@@ -28,8 +28,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
let node_path = require("node:path");
|
|
29
29
|
node_path = __toESM(node_path);
|
|
30
30
|
let _tailwindcss_language_service = require("@tailwindcss/language-service");
|
|
31
|
-
let
|
|
32
|
-
|
|
31
|
+
let ansis = require("ansis");
|
|
32
|
+
ansis = __toESM(ansis);
|
|
33
|
+
let tinyglobby = require("tinyglobby");
|
|
33
34
|
let vscode_languageserver_textdocument = require("vscode-languageserver-textdocument");
|
|
34
35
|
let node_module = require("node:module");
|
|
35
36
|
let node_fs = require("node:fs");
|
|
@@ -326,11 +327,11 @@ async function loadV3ClassMetadata(state, cwd, verbose = false) {
|
|
|
326
327
|
expandApplyAtRules: { module: generateRulesModule.expandApplyAtRules }
|
|
327
328
|
}
|
|
328
329
|
};
|
|
329
|
-
if (verbose) console.log(
|
|
330
|
+
if (verbose) console.log(ansis.default.dim(" ✓ Loaded v3 JIT modules"));
|
|
330
331
|
} catch (jitError) {
|
|
331
332
|
if (verbose) {
|
|
332
333
|
const message = jitError instanceof Error ? jitError.message : String(jitError);
|
|
333
|
-
console.log(
|
|
334
|
+
console.log(ansis.default.yellow(` ⚠ Warning: Could not load v3 JIT modules: ${message}`));
|
|
334
335
|
}
|
|
335
336
|
state.modules = { tailwindcss: {
|
|
336
337
|
version: state.version || "unknown",
|
|
@@ -344,11 +345,11 @@ async function loadV3ClassMetadata(state, cwd, verbose = false) {
|
|
|
344
345
|
};
|
|
345
346
|
if (state.modules?.jit?.createContext && state.config) try {
|
|
346
347
|
state.jitContext = state.modules.jit.createContext.module(state.config);
|
|
347
|
-
if (verbose) console.log(
|
|
348
|
+
if (verbose) console.log(ansis.default.dim(" ✓ Created JIT context"));
|
|
348
349
|
} catch (contextError) {
|
|
349
350
|
if (verbose) {
|
|
350
351
|
const message = contextError instanceof Error ? contextError.message : String(contextError);
|
|
351
|
-
console.log(
|
|
352
|
+
console.log(ansis.default.yellow(` ⚠ Warning: Could not create JIT context: ${message}`));
|
|
352
353
|
}
|
|
353
354
|
}
|
|
354
355
|
} catch (error) {
|
|
@@ -471,7 +472,7 @@ async function loadV4DesignSystem(state, cwd, configPath, verbose = false) {
|
|
|
471
472
|
});
|
|
472
473
|
state.designSystem = designSystem;
|
|
473
474
|
if (!state.classNames) state.classNames = { context: {} };
|
|
474
|
-
if (verbose) console.log(
|
|
475
|
+
if (verbose) console.log(ansis.default.dim(" ✓ Loaded v4 design system"));
|
|
475
476
|
} else throw new AdapterLoadError("v4", /* @__PURE__ */ new Error("Tailwind v4 __unstable__loadDesignSystem is not available. Please ensure you have Tailwind CSS v4 installed."));
|
|
476
477
|
} catch (error) {
|
|
477
478
|
if (error instanceof AdapterLoadError) throw error;
|
|
@@ -483,6 +484,7 @@ async function loadV4DesignSystem(state, cwd, configPath, verbose = false) {
|
|
|
483
484
|
//#endregion
|
|
484
485
|
//#region src/utils/config.ts
|
|
485
486
|
const require$2 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href || __filename);
|
|
487
|
+
const CONFIG_DISCOVERY_MAX_DEPTH = 8;
|
|
486
488
|
const isCssConfigFile = (filePath) => filePath.endsWith(".css");
|
|
487
489
|
async function loadTailwindConfig(configPath) {
|
|
488
490
|
if (isCssConfigFile(configPath)) return {};
|
|
@@ -509,6 +511,13 @@ async function findTailwindConfigPath(cwd, configPath) {
|
|
|
509
511
|
const fullPath = node_path.join(cwd, p);
|
|
510
512
|
if (fileExists(fullPath)) return fullPath;
|
|
511
513
|
}
|
|
514
|
+
const v3Recursive = await (0, tinyglobby.glob)(V3_CONFIG_PATHS.map((p) => `**/${p}`), {
|
|
515
|
+
cwd,
|
|
516
|
+
absolute: true,
|
|
517
|
+
ignore: DEFAULT_IGNORE_PATTERNS,
|
|
518
|
+
deep: CONFIG_DISCOVERY_MAX_DEPTH
|
|
519
|
+
});
|
|
520
|
+
if (v3Recursive.length > 0) return sortByPathDepth(v3Recursive)[0];
|
|
512
521
|
const v4Paths = V4_CSS_FOLDERS.flatMap((folder) => V4_CSS_NAMES.map((name) => node_path.join(folder, name)));
|
|
513
522
|
for (const p of v4Paths) {
|
|
514
523
|
const fullPath = node_path.join(cwd, p);
|
|
@@ -517,8 +526,52 @@ async function findTailwindConfigPath(cwd, configPath) {
|
|
|
517
526
|
if (TAILWIND_V4_IMPORT_REGEX.test(content)) return fullPath;
|
|
518
527
|
} catch {}
|
|
519
528
|
}
|
|
529
|
+
const cssCandidates = await (0, tinyglobby.glob)("**/*.css", {
|
|
530
|
+
cwd,
|
|
531
|
+
absolute: true,
|
|
532
|
+
ignore: DEFAULT_IGNORE_PATTERNS,
|
|
533
|
+
deep: CONFIG_DISCOVERY_MAX_DEPTH
|
|
534
|
+
});
|
|
535
|
+
const v4Matches = [];
|
|
536
|
+
for (const candidate of cssCandidates) try {
|
|
537
|
+
const content = readFileSync(candidate);
|
|
538
|
+
if (TAILWIND_V4_IMPORT_REGEX.test(content)) v4Matches.push(candidate);
|
|
539
|
+
} catch {}
|
|
540
|
+
if (v4Matches.length > 0) return sortCssCandidates(cwd, v4Matches)[0];
|
|
520
541
|
return null;
|
|
521
542
|
}
|
|
543
|
+
function sortByPathDepth(paths) {
|
|
544
|
+
return [...paths].sort((a, b) => {
|
|
545
|
+
const depthA = splitDepth(a);
|
|
546
|
+
const depthB = splitDepth(b);
|
|
547
|
+
if (depthA !== depthB) return depthA - depthB;
|
|
548
|
+
return a.localeCompare(b);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function sortCssCandidates(cwd, paths) {
|
|
552
|
+
return [...paths].sort((a, b) => {
|
|
553
|
+
const scoreA = cssCandidateScore(cwd, a);
|
|
554
|
+
const scoreB = cssCandidateScore(cwd, b);
|
|
555
|
+
if (scoreA !== scoreB) return scoreA - scoreB;
|
|
556
|
+
return a.localeCompare(b);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function cssCandidateScore(cwd, candidate) {
|
|
560
|
+
const normalized = node_path.relative(cwd, candidate).split(node_path.sep).join("/");
|
|
561
|
+
const base = node_path.basename(candidate);
|
|
562
|
+
const depth = splitDepth(normalized);
|
|
563
|
+
const nameScore = V4_CSS_NAMES.includes(base) ? 0 : 20;
|
|
564
|
+
const folderScore = isPreferredCssFolder(normalized) ? 0 : 10;
|
|
565
|
+
return depth * 100 + nameScore + folderScore;
|
|
566
|
+
}
|
|
567
|
+
function isPreferredCssFolder(relativePath) {
|
|
568
|
+
const folder = node_path.dirname(relativePath).replace(/\\/g, "/");
|
|
569
|
+
const withSlash = folder === "." ? "./" : `./${folder}/`;
|
|
570
|
+
return V4_CSS_FOLDERS.includes(withSlash);
|
|
571
|
+
}
|
|
572
|
+
function splitDepth(value) {
|
|
573
|
+
return value.split(/[\\/]/).filter(Boolean).length;
|
|
574
|
+
}
|
|
522
575
|
|
|
523
576
|
//#endregion
|
|
524
577
|
//#region src/state.ts
|
|
@@ -551,10 +604,10 @@ async function createState(cwd, configPath, verbose = false) {
|
|
|
551
604
|
const version = getTailwindVersion(cwd);
|
|
552
605
|
const isV4 = isV4Config(version);
|
|
553
606
|
if (verbose) {
|
|
554
|
-
console.log(
|
|
555
|
-
console.log(
|
|
556
|
-
console.log(
|
|
557
|
-
console.log(
|
|
607
|
+
console.log(ansis.default.cyan.bold("→ Tailwind Configuration"));
|
|
608
|
+
console.log(ansis.default.dim(` Version: ${version || "unknown"}`));
|
|
609
|
+
console.log(ansis.default.dim(` Config type: ${isCssConfig ? "CSS (v4)" : "JavaScript"}`));
|
|
610
|
+
console.log(ansis.default.dim(` Config path: ${resolvedConfigPath}`));
|
|
558
611
|
}
|
|
559
612
|
let config = {};
|
|
560
613
|
let resolvedConfig = { separator: ":" };
|
package/package.json
CHANGED
|
@@ -1,77 +1,81 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
2
|
+
"name": "tailwind-lint",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A command-line tool that uses the Tailwind CSS IntelliSense plugin to show linting suggestions for your Tailwind CSS classes",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"code-quality",
|
|
8
|
+
"css",
|
|
9
|
+
"diagnostics",
|
|
10
|
+
"intellisense",
|
|
11
|
+
"lint",
|
|
12
|
+
"linter",
|
|
13
|
+
"tailwind",
|
|
14
|
+
"tailwindcss",
|
|
15
|
+
"utility-first"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/ph1p/tailwind-lint#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/ph1p/tailwind-lint/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Philip Stapelfeldt <me@ph1p.dev>",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/ph1p/tailwind-lint.git"
|
|
26
|
+
},
|
|
27
|
+
"funding": {
|
|
28
|
+
"type": "github",
|
|
29
|
+
"url": "https://github.com/sponsors/ph1p"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"tailwind-lint": "./dist/cli.cjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"type": "module",
|
|
40
|
+
"types": "./dist/linter.d.ts",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"dev": "tsdown --watch",
|
|
48
|
+
"format": "oxfmt --write .",
|
|
49
|
+
"format:check": "oxfmt --check .",
|
|
50
|
+
"lint": "oxlint .",
|
|
51
|
+
"lint:fix": "oxlint --fix .",
|
|
52
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
53
|
+
"release": "pnpm dlx semantic-release",
|
|
54
|
+
"release:dry": "SEMREL_LOCAL=1 pnpm dlx semantic-release --dry-run --no-ci --repository-url .",
|
|
55
|
+
"start": "node dist/cli.cjs",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:coverage": "vitest run --coverage",
|
|
58
|
+
"test:watch": "vitest"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@tailwindcss/language-service": "^0.14.29",
|
|
62
|
+
"ansis": "^4.2.0",
|
|
63
|
+
"commander": "^14.0.3",
|
|
64
|
+
"postcss": "^8.5.8",
|
|
65
|
+
"tinyglobby": "^0.2.15",
|
|
66
|
+
"vscode-languageserver-textdocument": "^1.0.12"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "^25.3.3",
|
|
70
|
+
"oxfmt": "^0.36.0",
|
|
71
|
+
"oxlint": "^1.51.0",
|
|
72
|
+
"tsdown": "^0.20.3",
|
|
73
|
+
"typescript": "^5.9.3",
|
|
74
|
+
"vitest": "^4.0.18"
|
|
75
|
+
},
|
|
76
|
+
"engines": {
|
|
77
|
+
"node": ">=22.0.0",
|
|
78
|
+
"pnpm": ">=10.0.0"
|
|
79
|
+
},
|
|
80
|
+
"packageManager": "pnpm@10.30.3"
|
|
81
|
+
}
|