tailwind-lint 0.6.1 → 0.8.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 +58 -7
- package/dist/cli.cjs +107 -26
- package/dist/linter.cjs +168 -2
- package/dist/linter.d.cts +9 -1
- package/dist/{linter-DVb9soAi.cjs → state-QQi4_-5a.cjs} +143 -124
- package/package.json +80 -73
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ tailwind-lint --verbose
|
|
|
48
48
|
### How Auto-Discovery Works
|
|
49
49
|
|
|
50
50
|
**Tailwind CSS v4:**
|
|
51
|
+
|
|
51
52
|
- Finds CSS config files in common locations: `app.css`, `index.css`, `tailwind.css`, `global.css`, etc.
|
|
52
53
|
- Searches in project root and subdirectories: `./`, `./src/`, `./src/styles/`, `./app/`, etc.
|
|
53
54
|
- Uses file patterns from `@source` directives if present
|
|
@@ -55,6 +56,7 @@ tailwind-lint --verbose
|
|
|
55
56
|
- **Note:** When CSS config is in a subdirectory (e.g., `src/styles/global.css`), files are discovered from the project root
|
|
56
57
|
|
|
57
58
|
**Tailwind CSS v3:**
|
|
59
|
+
|
|
58
60
|
- Finds JavaScript config files: `tailwind.config.js`, `tailwind.config.cjs`, `tailwind.config.mjs`, `tailwind.config.ts`
|
|
59
61
|
- Uses file patterns from the `content` array in your config
|
|
60
62
|
|
|
@@ -63,6 +65,7 @@ tailwind-lint --verbose
|
|
|
63
65
|
- `-c, --config <path>` - Path to Tailwind config file (default: auto-discover)
|
|
64
66
|
- `-a, --auto` - Auto-discover files from config content patterns (legacy, enabled by default)
|
|
65
67
|
- `--fix` - Automatically fix problems that can be fixed
|
|
68
|
+
- `--format <text|json>` - Output format (`text` default, `json` for machine-readable output)
|
|
66
69
|
- `-v, --verbose` - Enable verbose logging for debugging
|
|
67
70
|
- `-h, --help` - Show help message
|
|
68
71
|
- `--version` - Show version number
|
|
@@ -81,8 +84,33 @@ tailwind-lint "**/*.vue" --fix
|
|
|
81
84
|
|
|
82
85
|
# Lint with a specific CSS config (v4)
|
|
83
86
|
tailwind-lint --config ./styles/app.css
|
|
87
|
+
|
|
88
|
+
# Machine-readable output for LLMs/agents
|
|
89
|
+
tailwind-lint --auto --format json
|
|
84
90
|
```
|
|
85
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
|
+
|
|
86
114
|
## Configuration
|
|
87
115
|
|
|
88
116
|
### Tailwind CSS v4
|
|
@@ -93,7 +121,7 @@ Create a CSS config file (`app.css`, `index.css`, or `tailwind.css`):
|
|
|
93
121
|
@import "tailwindcss";
|
|
94
122
|
|
|
95
123
|
@theme {
|
|
96
|
-
|
|
124
|
+
--color-primary: #3b82f6;
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
@source "./src/**/*.{js,jsx,ts,tsx,html}";
|
|
@@ -108,12 +136,12 @@ Create a JavaScript config file (`tailwind.config.js`):
|
|
|
108
136
|
```javascript
|
|
109
137
|
/** @type {import('tailwindcss').Config} */
|
|
110
138
|
module.exports = {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
139
|
+
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
140
|
+
theme: {
|
|
141
|
+
extend: {},
|
|
142
|
+
},
|
|
143
|
+
plugins: [],
|
|
144
|
+
};
|
|
117
145
|
```
|
|
118
146
|
|
|
119
147
|
## Autofix
|
|
@@ -128,6 +156,7 @@ Files are written atomically with multiple iterations to ensure all fixes are ap
|
|
|
128
156
|
## Features
|
|
129
157
|
|
|
130
158
|
**Core (v3 & v4):**
|
|
159
|
+
|
|
131
160
|
- CSS Conflicts - Detects when multiple classes apply the same CSS properties (e.g., `block flex`, `text-left text-center`) - **Note:** Works reliably in v4, limited support in v3 - no autofix
|
|
132
161
|
- Invalid @apply Usage - Validates if a class can be used with `@apply`
|
|
133
162
|
- Invalid @screen References - Detects references to non-existent breakpoints
|
|
@@ -138,6 +167,7 @@ Files are written atomically with multiple iterations to ensure all fixes are ap
|
|
|
138
167
|
- Autofix - Automatically fix issues with `--fix` flag
|
|
139
168
|
|
|
140
169
|
**v4-Specific:**
|
|
170
|
+
|
|
141
171
|
- Canonical Class Suggestions - Suggests shorthand equivalents for arbitrary values (e.g., `top-[60px]` → `top-15`)
|
|
142
172
|
- Invalid @source Directives - Validates `@source` directive paths
|
|
143
173
|
- Full theme loading - Automatically loads Tailwind's default theme
|
|
@@ -168,4 +198,25 @@ pnpm format
|
|
|
168
198
|
|
|
169
199
|
# Check code without fixing
|
|
170
200
|
pnpm lint
|
|
201
|
+
|
|
202
|
+
# Preview next version locally (no publish)
|
|
203
|
+
pnpm release:dry
|
|
171
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,27 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
2
|
+
const require_state = require('./state-QQi4_-5a.cjs');
|
|
3
|
+
const require_linter = require('./linter.cjs');
|
|
3
4
|
let node_path = require("node:path");
|
|
4
|
-
node_path =
|
|
5
|
+
node_path = require_state.__toESM(node_path);
|
|
5
6
|
let chalk = require("chalk");
|
|
6
|
-
chalk =
|
|
7
|
+
chalk = require_state.__toESM(chalk);
|
|
7
8
|
let node_fs = require("node:fs");
|
|
8
|
-
node_fs =
|
|
9
|
+
node_fs = require_state.__toESM(node_fs);
|
|
9
10
|
let commander = require("commander");
|
|
10
11
|
|
|
11
|
-
//#region src/
|
|
12
|
-
|
|
13
|
-
function countDiagnosticsBySeverity(diagnostics) {
|
|
12
|
+
//#region src/output.ts
|
|
13
|
+
function countBySeverity(diagnostics) {
|
|
14
14
|
let errors = 0;
|
|
15
15
|
let warnings = 0;
|
|
16
16
|
for (const diagnostic of diagnostics) {
|
|
17
|
-
if (diagnostic.severity ===
|
|
18
|
-
if (diagnostic.severity ===
|
|
17
|
+
if (diagnostic.severity === require_state.SEVERITY.ERROR) errors++;
|
|
18
|
+
if (diagnostic.severity === require_state.SEVERITY.WARNING) warnings++;
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
21
|
errors,
|
|
22
22
|
warnings
|
|
23
23
|
};
|
|
24
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;
|
|
25
83
|
function resolveOptions(files, options) {
|
|
26
84
|
const hasConfigFlag = !!options.config;
|
|
27
85
|
const hasAutoFlag = !!options.auto;
|
|
@@ -33,7 +91,7 @@ function resolveOptions(files, options) {
|
|
|
33
91
|
const absoluteConfigPath = node_path.isAbsolute(options.config) ? options.config : node_path.resolve(process.cwd(), options.config);
|
|
34
92
|
cwd = node_path.dirname(absoluteConfigPath);
|
|
35
93
|
configPath = node_path.basename(absoluteConfigPath);
|
|
36
|
-
patterns = [
|
|
94
|
+
patterns = [require_state.DEFAULT_FILE_PATTERN];
|
|
37
95
|
}
|
|
38
96
|
const autoDiscover = hasAutoFlag;
|
|
39
97
|
return {
|
|
@@ -65,15 +123,15 @@ async function displayResults(files, fixMode) {
|
|
|
65
123
|
console.log(chalk.default.green(` ✔ Fixed ${issueText}`));
|
|
66
124
|
totalFixed += file.fixedCount || 0;
|
|
67
125
|
}
|
|
68
|
-
const { errors, warnings } =
|
|
126
|
+
const { errors, warnings } = countBySeverity(file.diagnostics);
|
|
69
127
|
totalErrors += errors;
|
|
70
128
|
totalWarnings += warnings;
|
|
71
129
|
if (file.diagnostics.length > 0) filesWithIssues++;
|
|
72
130
|
for (const diagnostic of file.diagnostics) {
|
|
73
131
|
const line = diagnostic.range.start.line + 1;
|
|
74
132
|
const char = diagnostic.range.start.character + 1;
|
|
75
|
-
const severity = diagnostic.severity ===
|
|
76
|
-
const severityColor = diagnostic.severity ===
|
|
133
|
+
const severity = diagnostic.severity === require_state.SEVERITY.ERROR ? "error" : "warning";
|
|
134
|
+
const severityColor = diagnostic.severity === require_state.SEVERITY.ERROR ? chalk.default.red(severity) : chalk.default.yellow(severity);
|
|
77
135
|
const code = diagnostic.code ? chalk.default.dim(` (${diagnostic.code})`) : "";
|
|
78
136
|
console.log(` ${chalk.default.dim(`${line}:${char}`)} ${severityColor} ${diagnostic.message}${code}`);
|
|
79
137
|
}
|
|
@@ -100,9 +158,9 @@ const program = new commander.Command();
|
|
|
100
158
|
const getVersion = () => {
|
|
101
159
|
const packageJsonPath = node_path.join(__dirname, "../package.json");
|
|
102
160
|
try {
|
|
103
|
-
return JSON.parse(node_fs.readFileSync(packageJsonPath, "utf-8")).version ||
|
|
161
|
+
return JSON.parse(node_fs.readFileSync(packageJsonPath, "utf-8")).version || require_state.DEFAULT_VERSION;
|
|
104
162
|
} catch {
|
|
105
|
-
return
|
|
163
|
+
return require_state.DEFAULT_VERSION;
|
|
106
164
|
}
|
|
107
165
|
};
|
|
108
166
|
program.configureHelp({ formatHelp: (cmd, helper) => {
|
|
@@ -131,7 +189,7 @@ program.configureHelp({ formatHelp: (cmd, helper) => {
|
|
|
131
189
|
}
|
|
132
190
|
return output;
|
|
133
191
|
} });
|
|
134
|
-
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", `
|
|
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", `
|
|
135
193
|
${chalk.default.bold.cyan("Examples:")}
|
|
136
194
|
${chalk.default.dim("$")} tailwind-lint
|
|
137
195
|
${chalk.default.dim("$")} tailwind-lint ${chalk.default.green("\"src/**/*.{js,jsx,ts,tsx}\"")}
|
|
@@ -150,8 +208,9 @@ ${chalk.default.bold.cyan("Notes:")}
|
|
|
150
208
|
const hasAutoFlag = !!options.auto;
|
|
151
209
|
if (!(files.length > 0) && !hasAutoFlag && !hasConfigFlag) options.auto = true;
|
|
152
210
|
const resolved = resolveOptions(files, options);
|
|
211
|
+
const isJsonOutput = (options.format === "json" ? "json" : "text") === "json";
|
|
153
212
|
try {
|
|
154
|
-
if (resolved.verbose) {
|
|
213
|
+
if (resolved.verbose && !isJsonOutput) {
|
|
155
214
|
console.log(chalk.default.cyan.bold("→ Configuration"));
|
|
156
215
|
console.log(chalk.default.dim(` Working directory: ${resolved.cwd}`));
|
|
157
216
|
console.log(chalk.default.dim(` Config path: ${resolved.configPath || "auto-discover"}`));
|
|
@@ -162,29 +221,51 @@ ${chalk.default.bold.cyan("Notes:")}
|
|
|
162
221
|
const results = await require_linter.lint({
|
|
163
222
|
...resolved,
|
|
164
223
|
onProgress: (current, total, file) => {
|
|
224
|
+
if (isJsonOutput) return;
|
|
165
225
|
if (process.stdout.isTTY && !resolved.verbose) {
|
|
166
226
|
const displayFile = truncateFilename(file);
|
|
167
|
-
process.stdout.write(`\r${chalk.default.cyan("→")} Linting files... ${chalk.default.dim(`(${current}/${total})`)} ${chalk.default.dim(displayFile)}${" ".repeat(
|
|
227
|
+
process.stdout.write(`\r${chalk.default.cyan("→")} Linting files... ${chalk.default.dim(`(${current}/${total})`)} ${chalk.default.dim(displayFile)}${" ".repeat(require_state.TERMINAL_PADDING)}`);
|
|
168
228
|
} else if (resolved.verbose) console.log(chalk.default.dim(` [${current}/${total}] Linting ${file}`));
|
|
169
229
|
}
|
|
170
230
|
});
|
|
171
|
-
if (process.stdout.isTTY && !resolved.verbose) process.stdout.write(`\r${" ".repeat(
|
|
231
|
+
if (process.stdout.isTTY && !resolved.verbose && !isJsonOutput) process.stdout.write(`\r${" ".repeat(require_state.TERMINAL_WIDTH)}\r`);
|
|
172
232
|
if (results.totalFilesProcessed === 0) {
|
|
173
|
-
console.log(
|
|
174
|
-
|
|
233
|
+
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
234
|
+
...resolved,
|
|
235
|
+
files: [],
|
|
236
|
+
totalFilesProcessed: 0
|
|
237
|
+
})));
|
|
238
|
+
else {
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(chalk.default.yellow("⚠ No files found to lint"));
|
|
241
|
+
}
|
|
175
242
|
process.exit(0);
|
|
176
243
|
}
|
|
177
244
|
if (results.files.length === 0) {
|
|
178
|
-
console.log(
|
|
245
|
+
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
246
|
+
...resolved,
|
|
247
|
+
files: [],
|
|
248
|
+
totalFilesProcessed: results.totalFilesProcessed
|
|
249
|
+
})));
|
|
250
|
+
else console.log(chalk.default.green.bold("✔ No issues found"));
|
|
179
251
|
process.exit(0);
|
|
180
252
|
}
|
|
181
|
-
|
|
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);
|
|
259
|
+
const hasErrors = results.files.some((file) => file.diagnostics.some((d) => d.severity === require_state.SEVERITY.ERROR));
|
|
183
260
|
process.exit(hasErrors ? 1 : 0);
|
|
184
261
|
} catch (error) {
|
|
185
262
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
186
|
-
console.
|
|
187
|
-
|
|
263
|
+
if (isJsonOutput) console.log(JSON.stringify({
|
|
264
|
+
ok: false,
|
|
265
|
+
error: errorMessage
|
|
266
|
+
}));
|
|
267
|
+
else console.error(chalk.default.red("✖ Error:"), errorMessage);
|
|
268
|
+
if (resolved.verbose && error instanceof Error && !isJsonOutput) {
|
|
188
269
|
console.error(chalk.default.dim("\nStack trace:"));
|
|
189
270
|
console.error(chalk.default.dim(error.stack || error.toString()));
|
|
190
271
|
}
|
package/dist/linter.cjs
CHANGED
|
@@ -1,3 +1,169 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_state = require('./state-QQi4_-5a.cjs');
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
node_path = require_state.__toESM(node_path);
|
|
5
|
+
let _tailwindcss_language_service = require("@tailwindcss/language-service");
|
|
6
|
+
let chalk = require("chalk");
|
|
7
|
+
chalk = require_state.__toESM(chalk);
|
|
8
|
+
let fast_glob = require("fast-glob");
|
|
9
|
+
fast_glob = require_state.__toESM(fast_glob);
|
|
10
|
+
let vscode_languageserver_textdocument = require("vscode-languageserver-textdocument");
|
|
2
11
|
|
|
3
|
-
|
|
12
|
+
//#region src/linter.ts
|
|
13
|
+
async function validateDocument(state, filePath, content) {
|
|
14
|
+
try {
|
|
15
|
+
if (!state) throw new Error("State is not initialized");
|
|
16
|
+
if (state.v4 && !state.designSystem) throw new Error("Design system not initialized for Tailwind v4. This might indicate a configuration issue.");
|
|
17
|
+
if (!state.v4 && !state.modules?.tailwindcss) throw new Error("Tailwind modules not initialized for Tailwind v3. This might indicate a configuration issue.");
|
|
18
|
+
const languageId = require_state.getLanguageId(filePath);
|
|
19
|
+
const uri = `file://${filePath}`;
|
|
20
|
+
return await (0, _tailwindcss_language_service.doValidate)(state, vscode_languageserver_textdocument.TextDocument.create(uri, languageId, 1, content));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
if (message.includes("Cannot read") || message.includes("undefined")) {
|
|
24
|
+
if (process.env.DEBUG) console.error(`Debug: Language service error for ${filePath}:`, error);
|
|
25
|
+
console.warn(`Warning: Language service crashed while validating ${filePath}. Skipping this file.`);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Failed to validate document ${filePath}: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function discoverFiles(cwd, patterns, configPath, autoDiscover) {
|
|
32
|
+
if (autoDiscover) return discoverFilesFromConfig(cwd, configPath);
|
|
33
|
+
return expandPatterns(cwd, patterns);
|
|
34
|
+
}
|
|
35
|
+
async function expandPatterns(cwd, patterns, extraIgnore = []) {
|
|
36
|
+
const files = await (0, fast_glob.default)(patterns, {
|
|
37
|
+
cwd,
|
|
38
|
+
absolute: false,
|
|
39
|
+
ignore: [...require_state.DEFAULT_IGNORE_PATTERNS, ...extraIgnore]
|
|
40
|
+
});
|
|
41
|
+
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
42
|
+
}
|
|
43
|
+
async function discoverFilesFromConfig(cwd, configPath) {
|
|
44
|
+
const configFilePath = await require_state.findTailwindConfigPath(cwd, configPath);
|
|
45
|
+
if (!configFilePath) throw new Error("Could not find Tailwind config for auto-discovery.\nUse --config to specify the path, or provide file patterns directly.");
|
|
46
|
+
if (!require_state.isCssConfigFile(configFilePath)) {
|
|
47
|
+
const config = await require_state.loadTailwindConfig(configFilePath);
|
|
48
|
+
if (!config || !config.content) throw new Error("Tailwind config is missing the 'content' property.\nAdd a content array to specify which files to scan:\n content: ['./src/**/*.{js,jsx,ts,tsx}']");
|
|
49
|
+
const patterns = extractContentPatterns(config);
|
|
50
|
+
if (patterns.length === 0) throw new Error("No content patterns found in Tailwind config.\nEnsure your config has a content array with file patterns.");
|
|
51
|
+
return expandPatterns(cwd, patterns);
|
|
52
|
+
}
|
|
53
|
+
const configDir = node_path.dirname(configFilePath);
|
|
54
|
+
const cssContent = require_state.readFileSync(configFilePath);
|
|
55
|
+
const { include, exclude } = extractSourcePatterns(cssContent);
|
|
56
|
+
const importSource = extractImportSourceDirectives(cssContent);
|
|
57
|
+
const resolveFromConfig = (pattern) => {
|
|
58
|
+
const absolutePattern = node_path.resolve(configDir, pattern);
|
|
59
|
+
return node_path.relative(cwd, absolutePattern);
|
|
60
|
+
};
|
|
61
|
+
const resolvedExclude = exclude.map(resolveFromConfig);
|
|
62
|
+
const gitignorePatterns = require_state.readGitignorePatterns(cwd);
|
|
63
|
+
const extraIgnore = [...resolvedExclude, ...gitignorePatterns];
|
|
64
|
+
if (include.length > 0) return expandPatterns(cwd, include.map(resolveFromConfig), extraIgnore);
|
|
65
|
+
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);
|
|
66
|
+
if (importSource.disableAutoSource) return [];
|
|
67
|
+
return expandPatterns(cwd, [require_state.DEFAULT_FILE_PATTERN], extraIgnore);
|
|
68
|
+
}
|
|
69
|
+
function extractContentPatterns(config) {
|
|
70
|
+
if (!config.content) return [];
|
|
71
|
+
return (Array.isArray(config.content) ? config.content : config.content.files || []).filter((p) => typeof p === "string");
|
|
72
|
+
}
|
|
73
|
+
function extractSourcePatterns(cssContent) {
|
|
74
|
+
const include = [];
|
|
75
|
+
const exclude = [];
|
|
76
|
+
for (const match of cssContent.matchAll(/@source\s+(not\s+)?(?:inline\(|["']([^"']+)["'])/g)) {
|
|
77
|
+
const isNot = !!match[1];
|
|
78
|
+
const filePath = match[2];
|
|
79
|
+
if (!filePath) continue;
|
|
80
|
+
if (isNot) exclude.push(filePath);
|
|
81
|
+
else include.push(filePath);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
include,
|
|
85
|
+
exclude
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function extractImportSourceDirectives(cssContent) {
|
|
89
|
+
const roots = [];
|
|
90
|
+
let disableAutoSource = false;
|
|
91
|
+
for (const match of cssContent.matchAll(/@import\s+["']tailwindcss(?:[^;]*?)\ssource\(\s*(none|["'][^"']+["'])\s*\)/g)) {
|
|
92
|
+
const raw = match[1];
|
|
93
|
+
if (!raw) continue;
|
|
94
|
+
if (raw === "none") {
|
|
95
|
+
disableAutoSource = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const sourceRoot = raw.slice(1, -1).trim();
|
|
99
|
+
if (sourceRoot.length > 0) roots.push(sourceRoot);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
roots: [...new Set(roots)],
|
|
103
|
+
disableAutoSource
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function processFiles(state, cwd, files, fix, onProgress) {
|
|
107
|
+
const results = [];
|
|
108
|
+
for (let i = 0; i < files.length; i += require_state.CONCURRENT_FILES) {
|
|
109
|
+
const batch = files.slice(i, i + require_state.CONCURRENT_FILES);
|
|
110
|
+
const batchResults = await Promise.all(batch.map(async (file, batchIndex) => {
|
|
111
|
+
if (onProgress) onProgress(i + batchIndex + 1, files.length, file);
|
|
112
|
+
return processFile(state, cwd, file, fix);
|
|
113
|
+
}));
|
|
114
|
+
results.push(...batchResults.filter((r) => r !== null));
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
async function processFile(state, cwd, filePath, fix) {
|
|
119
|
+
const absolutePath = node_path.isAbsolute(filePath) ? filePath : node_path.resolve(cwd, filePath);
|
|
120
|
+
if (!require_state.fileExists(absolutePath)) return null;
|
|
121
|
+
const content = require_state.readFileSync(absolutePath);
|
|
122
|
+
let diagnostics = await validateDocument(state, absolutePath, content);
|
|
123
|
+
let fixedCount = 0;
|
|
124
|
+
if (fix && diagnostics.length > 0) {
|
|
125
|
+
const fixResult = await require_state.applyCodeActions(state, absolutePath, content, diagnostics);
|
|
126
|
+
if (fixResult.changed) {
|
|
127
|
+
require_state.writeFileSync(absolutePath, fixResult.content);
|
|
128
|
+
fixedCount = fixResult.fixedCount;
|
|
129
|
+
diagnostics = await validateDocument(state, absolutePath, fixResult.content);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
path: node_path.relative(cwd, absolutePath),
|
|
134
|
+
diagnostics,
|
|
135
|
+
fixed: fixedCount > 0,
|
|
136
|
+
fixedCount
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function initializeState(cwd, configPath, verbose = false) {
|
|
140
|
+
try {
|
|
141
|
+
const state = await require_state.createState(cwd, configPath, verbose);
|
|
142
|
+
if (verbose) console.log();
|
|
143
|
+
return state;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
throw new Error(`Failed to initialize Tailwind state: ${message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verbose = false, onProgress }) {
|
|
150
|
+
const state = await initializeState(cwd, configPath, verbose);
|
|
151
|
+
const files = await discoverFiles(cwd, patterns, configPath, autoDiscover);
|
|
152
|
+
if (verbose) {
|
|
153
|
+
console.log(chalk.default.cyan.bold(`→ Discovered ${files.length} file${files.length !== 1 ? "s" : ""} to lint`));
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
if (files.length === 0) return {
|
|
157
|
+
files: [],
|
|
158
|
+
totalFilesProcessed: 0
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
files: (await processFiles(state, cwd, files, fix, onProgress)).filter((result) => result.diagnostics.length > 0 || result.fixed),
|
|
162
|
+
totalFilesProcessed: files.length
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
exports.extractImportSourceDirectives = extractImportSourceDirectives;
|
|
168
|
+
exports.extractSourcePatterns = extractSourcePatterns;
|
|
169
|
+
exports.lint = lint;
|
package/dist/linter.d.cts
CHANGED
|
@@ -22,6 +22,14 @@ interface LintResult {
|
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/linter.d.ts
|
|
25
|
+
declare function extractSourcePatterns(cssContent: string): {
|
|
26
|
+
include: string[];
|
|
27
|
+
exclude: string[];
|
|
28
|
+
};
|
|
29
|
+
declare function extractImportSourceDirectives(cssContent: string): {
|
|
30
|
+
roots: string[];
|
|
31
|
+
disableAutoSource: boolean;
|
|
32
|
+
};
|
|
25
33
|
declare function lint({
|
|
26
34
|
cwd,
|
|
27
35
|
patterns,
|
|
@@ -32,4 +40,4 @@ declare function lint({
|
|
|
32
40
|
onProgress
|
|
33
41
|
}: LintOptions): Promise<LintResult>;
|
|
34
42
|
//#endregion
|
|
35
|
-
export { type LintFileResult, type LintOptions, type LintResult, lint };
|
|
43
|
+
export { type LintFileResult, type LintOptions, type LintResult, extractImportSourceDirectives, extractSourcePatterns, lint };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
2
|
var __create = Object.create;
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -68,11 +68,13 @@ const V4_CSS_NAMES = [
|
|
|
68
68
|
"global.css",
|
|
69
69
|
"styles.css",
|
|
70
70
|
"style.css",
|
|
71
|
-
"main.css"
|
|
71
|
+
"main.css",
|
|
72
|
+
"input.css"
|
|
72
73
|
];
|
|
73
74
|
const V4_CSS_FOLDERS = [
|
|
74
75
|
"./",
|
|
75
76
|
"./src/",
|
|
77
|
+
"./src/app/",
|
|
76
78
|
"./src/css/",
|
|
77
79
|
"./src/style/",
|
|
78
80
|
"./src/styles/",
|
|
@@ -80,9 +82,12 @@ const V4_CSS_FOLDERS = [
|
|
|
80
82
|
"./app/css/",
|
|
81
83
|
"./app/style/",
|
|
82
84
|
"./app/styles/",
|
|
85
|
+
"./app/assets/css/",
|
|
83
86
|
"./css/",
|
|
84
87
|
"./style/",
|
|
85
|
-
"./styles/"
|
|
88
|
+
"./styles/",
|
|
89
|
+
"./assets/css/",
|
|
90
|
+
"./resources/css/"
|
|
86
91
|
];
|
|
87
92
|
const LANGUAGE_MAP = {
|
|
88
93
|
".astro": "astro",
|
|
@@ -118,6 +123,7 @@ const TERMINAL_WIDTH = 80;
|
|
|
118
123
|
const TERMINAL_PADDING = 10;
|
|
119
124
|
const MAX_FIX_ITERATIONS = 100;
|
|
120
125
|
const QUICKFIX_ACTION_KIND = "quickfix";
|
|
126
|
+
const TAILWIND_V4_IMPORT_REGEX = /@import\s+["']tailwindcss(?:["'\s/]|$)/;
|
|
121
127
|
function getLanguageId(filePath) {
|
|
122
128
|
return LANGUAGE_MAP[filePath.substring(filePath.lastIndexOf(".")).toLowerCase()] || "html";
|
|
123
129
|
}
|
|
@@ -386,6 +392,19 @@ function writeFileSync(filePath, content) {
|
|
|
386
392
|
if (typeof content !== "string") throw new TypeError("Content must be a string");
|
|
387
393
|
node_fs.writeFileSync(filePath, content, "utf-8");
|
|
388
394
|
}
|
|
395
|
+
function readGitignorePatterns(cwd) {
|
|
396
|
+
const gitignorePath = node_path.join(cwd, ".gitignore");
|
|
397
|
+
if (!fileExists(gitignorePath)) return [];
|
|
398
|
+
try {
|
|
399
|
+
return node_fs.readFileSync(gitignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!")).map((pattern) => {
|
|
400
|
+
const cleaned = pattern.replace(/\/+$/, "");
|
|
401
|
+
if (cleaned.includes("/") || cleaned.includes("*")) return cleaned.endsWith("/**") ? cleaned : `${cleaned}/**`;
|
|
402
|
+
return `**/${cleaned}/**`;
|
|
403
|
+
});
|
|
404
|
+
} catch {
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
389
408
|
|
|
390
409
|
//#endregion
|
|
391
410
|
//#region src/adapters/v4-adapter.ts
|
|
@@ -466,6 +485,7 @@ async function loadV4DesignSystem(state, cwd, configPath, verbose = false) {
|
|
|
466
485
|
//#endregion
|
|
467
486
|
//#region src/utils/config.ts
|
|
468
487
|
const require$2 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href || __filename);
|
|
488
|
+
const CONFIG_DISCOVERY_MAX_DEPTH = 8;
|
|
469
489
|
const isCssConfigFile = (filePath) => filePath.endsWith(".css");
|
|
470
490
|
async function loadTailwindConfig(configPath) {
|
|
471
491
|
if (isCssConfigFile(configPath)) return {};
|
|
@@ -492,16 +512,67 @@ async function findTailwindConfigPath(cwd, configPath) {
|
|
|
492
512
|
const fullPath = node_path.join(cwd, p);
|
|
493
513
|
if (fileExists(fullPath)) return fullPath;
|
|
494
514
|
}
|
|
515
|
+
const v3Recursive = await (0, fast_glob.default)(V3_CONFIG_PATHS.map((p) => `**/${p}`), {
|
|
516
|
+
cwd,
|
|
517
|
+
absolute: true,
|
|
518
|
+
ignore: DEFAULT_IGNORE_PATTERNS,
|
|
519
|
+
deep: CONFIG_DISCOVERY_MAX_DEPTH
|
|
520
|
+
});
|
|
521
|
+
if (v3Recursive.length > 0) return sortByPathDepth(v3Recursive)[0];
|
|
495
522
|
const v4Paths = V4_CSS_FOLDERS.flatMap((folder) => V4_CSS_NAMES.map((name) => node_path.join(folder, name)));
|
|
496
523
|
for (const p of v4Paths) {
|
|
497
524
|
const fullPath = node_path.join(cwd, p);
|
|
498
525
|
if (fileExists(fullPath)) try {
|
|
499
526
|
const content = readFileSync(fullPath);
|
|
500
|
-
if (
|
|
527
|
+
if (TAILWIND_V4_IMPORT_REGEX.test(content)) return fullPath;
|
|
501
528
|
} catch {}
|
|
502
529
|
}
|
|
530
|
+
const cssCandidates = await (0, fast_glob.default)("**/*.css", {
|
|
531
|
+
cwd,
|
|
532
|
+
absolute: true,
|
|
533
|
+
ignore: DEFAULT_IGNORE_PATTERNS,
|
|
534
|
+
deep: CONFIG_DISCOVERY_MAX_DEPTH
|
|
535
|
+
});
|
|
536
|
+
const v4Matches = [];
|
|
537
|
+
for (const candidate of cssCandidates) try {
|
|
538
|
+
const content = readFileSync(candidate);
|
|
539
|
+
if (TAILWIND_V4_IMPORT_REGEX.test(content)) v4Matches.push(candidate);
|
|
540
|
+
} catch {}
|
|
541
|
+
if (v4Matches.length > 0) return sortCssCandidates(cwd, v4Matches)[0];
|
|
503
542
|
return null;
|
|
504
543
|
}
|
|
544
|
+
function sortByPathDepth(paths) {
|
|
545
|
+
return [...paths].sort((a, b) => {
|
|
546
|
+
const depthA = splitDepth(a);
|
|
547
|
+
const depthB = splitDepth(b);
|
|
548
|
+
if (depthA !== depthB) return depthA - depthB;
|
|
549
|
+
return a.localeCompare(b);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function sortCssCandidates(cwd, paths) {
|
|
553
|
+
return [...paths].sort((a, b) => {
|
|
554
|
+
const scoreA = cssCandidateScore(cwd, a);
|
|
555
|
+
const scoreB = cssCandidateScore(cwd, b);
|
|
556
|
+
if (scoreA !== scoreB) return scoreA - scoreB;
|
|
557
|
+
return a.localeCompare(b);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function cssCandidateScore(cwd, candidate) {
|
|
561
|
+
const normalized = node_path.relative(cwd, candidate).split(node_path.sep).join("/");
|
|
562
|
+
const base = node_path.basename(candidate);
|
|
563
|
+
const depth = splitDepth(normalized);
|
|
564
|
+
const nameScore = V4_CSS_NAMES.includes(base) ? 0 : 20;
|
|
565
|
+
const folderScore = isPreferredCssFolder(normalized) ? 0 : 10;
|
|
566
|
+
return depth * 100 + nameScore + folderScore;
|
|
567
|
+
}
|
|
568
|
+
function isPreferredCssFolder(relativePath) {
|
|
569
|
+
const folder = node_path.dirname(relativePath).replace(/\\/g, "/");
|
|
570
|
+
const withSlash = folder === "." ? "./" : `./${folder}/`;
|
|
571
|
+
return V4_CSS_FOLDERS.includes(withSlash);
|
|
572
|
+
}
|
|
573
|
+
function splitDepth(value) {
|
|
574
|
+
return value.split(/[\\/]/).filter(Boolean).length;
|
|
575
|
+
}
|
|
505
576
|
|
|
506
577
|
//#endregion
|
|
507
578
|
//#region src/state.ts
|
|
@@ -571,130 +642,24 @@ async function createState(cwd, configPath, verbose = false) {
|
|
|
571
642
|
}
|
|
572
643
|
|
|
573
644
|
//#endregion
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const languageId = getLanguageId(filePath);
|
|
581
|
-
const uri = `file://${filePath}`;
|
|
582
|
-
return await (0, _tailwindcss_language_service.doValidate)(state, vscode_languageserver_textdocument.TextDocument.create(uri, languageId, 1, content));
|
|
583
|
-
} catch (error) {
|
|
584
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
585
|
-
if (message.includes("Cannot read") || message.includes("undefined")) {
|
|
586
|
-
if (process.env.DEBUG) console.error(`Debug: Language service error for ${filePath}:`, error);
|
|
587
|
-
console.warn(`Warning: Language service crashed while validating ${filePath}. Skipping this file.`);
|
|
588
|
-
return [];
|
|
589
|
-
}
|
|
590
|
-
throw new Error(`Failed to validate document ${filePath}: ${message}`);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
async function discoverFiles(cwd, patterns, configPath, autoDiscover) {
|
|
594
|
-
if (autoDiscover) return discoverFilesFromConfig(cwd, configPath);
|
|
595
|
-
return expandPatterns(cwd, patterns);
|
|
596
|
-
}
|
|
597
|
-
async function expandPatterns(cwd, patterns) {
|
|
598
|
-
return (0, fast_glob.default)(patterns, {
|
|
599
|
-
cwd,
|
|
600
|
-
absolute: false,
|
|
601
|
-
ignore: DEFAULT_IGNORE_PATTERNS
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
async function discoverFilesFromConfig(cwd, configPath) {
|
|
605
|
-
const configFilePath = await findTailwindConfigPath(cwd, configPath);
|
|
606
|
-
if (!configFilePath) throw new Error("Could not find Tailwind config for auto-discovery.\nUse --config to specify the path, or provide file patterns directly.");
|
|
607
|
-
if (!isCssConfigFile(configFilePath)) {
|
|
608
|
-
const config = await loadTailwindConfig(configFilePath);
|
|
609
|
-
if (!config || !config.content) throw new Error("Tailwind config is missing the 'content' property.\nAdd a content array to specify which files to scan:\n content: ['./src/**/*.{js,jsx,ts,tsx}']");
|
|
610
|
-
const patterns = extractContentPatterns(config);
|
|
611
|
-
if (patterns.length === 0) throw new Error("No content patterns found in Tailwind config.\nEnsure your config has a content array with file patterns.");
|
|
612
|
-
return expandPatterns(cwd, patterns);
|
|
613
|
-
}
|
|
614
|
-
const configDir = node_path.dirname(configFilePath);
|
|
615
|
-
const sourcePatterns = extractSourcePatterns(readFileSync(configFilePath));
|
|
616
|
-
if (sourcePatterns.length > 0) return expandPatterns(cwd, sourcePatterns.map((pattern) => {
|
|
617
|
-
const absolutePattern = node_path.resolve(configDir, pattern);
|
|
618
|
-
return node_path.relative(cwd, absolutePattern);
|
|
619
|
-
}));
|
|
620
|
-
return expandPatterns(cwd, [DEFAULT_FILE_PATTERN]);
|
|
621
|
-
}
|
|
622
|
-
function extractContentPatterns(config) {
|
|
623
|
-
if (!config.content) return [];
|
|
624
|
-
return (Array.isArray(config.content) ? config.content : config.content.files || []).filter((p) => typeof p === "string");
|
|
625
|
-
}
|
|
626
|
-
function extractSourcePatterns(cssContent) {
|
|
627
|
-
const patterns = [];
|
|
628
|
-
for (const match of cssContent.matchAll(/@source\s+["']([^"']+)["']/g)) patterns.push(match[1]);
|
|
629
|
-
return patterns;
|
|
630
|
-
}
|
|
631
|
-
async function processFiles(state, cwd, files, fix, onProgress) {
|
|
632
|
-
const results = [];
|
|
633
|
-
for (let i = 0; i < files.length; i += CONCURRENT_FILES) {
|
|
634
|
-
const batch = files.slice(i, i + CONCURRENT_FILES);
|
|
635
|
-
const batchResults = await Promise.all(batch.map(async (file, batchIndex) => {
|
|
636
|
-
if (onProgress) onProgress(i + batchIndex + 1, files.length, file);
|
|
637
|
-
return processFile(state, cwd, file, fix);
|
|
638
|
-
}));
|
|
639
|
-
results.push(...batchResults.filter((r) => r !== null));
|
|
640
|
-
}
|
|
641
|
-
return results;
|
|
642
|
-
}
|
|
643
|
-
async function processFile(state, cwd, filePath, fix) {
|
|
644
|
-
const absolutePath = node_path.isAbsolute(filePath) ? filePath : node_path.resolve(cwd, filePath);
|
|
645
|
-
if (!fileExists(absolutePath)) return null;
|
|
646
|
-
const content = readFileSync(absolutePath);
|
|
647
|
-
let diagnostics = await validateDocument(state, absolutePath, content);
|
|
648
|
-
let fixedCount = 0;
|
|
649
|
-
if (fix && diagnostics.length > 0) {
|
|
650
|
-
const fixResult = await applyCodeActions(state, absolutePath, content, diagnostics);
|
|
651
|
-
if (fixResult.changed) {
|
|
652
|
-
writeFileSync(absolutePath, fixResult.content);
|
|
653
|
-
fixedCount = fixResult.fixedCount;
|
|
654
|
-
diagnostics = await validateDocument(state, absolutePath, fixResult.content);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return {
|
|
658
|
-
path: node_path.relative(cwd, absolutePath),
|
|
659
|
-
diagnostics,
|
|
660
|
-
fixed: fixedCount > 0,
|
|
661
|
-
fixedCount
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
async function initializeState(cwd, configPath, verbose = false) {
|
|
665
|
-
try {
|
|
666
|
-
const state = await createState(cwd, configPath, verbose);
|
|
667
|
-
if (verbose) console.log();
|
|
668
|
-
return state;
|
|
669
|
-
} catch (error) {
|
|
670
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
671
|
-
throw new Error(`Failed to initialize Tailwind state: ${message}`);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verbose = false, onProgress }) {
|
|
675
|
-
const state = await initializeState(cwd, configPath, verbose);
|
|
676
|
-
const files = await discoverFiles(cwd, patterns, configPath, autoDiscover);
|
|
677
|
-
if (verbose) {
|
|
678
|
-
console.log(chalk.default.cyan.bold(`→ Discovered ${files.length} file${files.length !== 1 ? "s" : ""} to lint`));
|
|
679
|
-
console.log();
|
|
680
|
-
}
|
|
681
|
-
if (files.length === 0) return {
|
|
682
|
-
files: [],
|
|
683
|
-
totalFilesProcessed: 0
|
|
684
|
-
};
|
|
685
|
-
return {
|
|
686
|
-
files: (await processFiles(state, cwd, files, fix, onProgress)).filter((result) => result.diagnostics.length > 0 || result.fixed),
|
|
687
|
-
totalFilesProcessed: files.length
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
//#endregion
|
|
645
|
+
Object.defineProperty(exports, 'CONCURRENT_FILES', {
|
|
646
|
+
enumerable: true,
|
|
647
|
+
get: function () {
|
|
648
|
+
return CONCURRENT_FILES;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
692
651
|
Object.defineProperty(exports, 'DEFAULT_FILE_PATTERN', {
|
|
693
652
|
enumerable: true,
|
|
694
653
|
get: function () {
|
|
695
654
|
return DEFAULT_FILE_PATTERN;
|
|
696
655
|
}
|
|
697
656
|
});
|
|
657
|
+
Object.defineProperty(exports, 'DEFAULT_IGNORE_PATTERNS', {
|
|
658
|
+
enumerable: true,
|
|
659
|
+
get: function () {
|
|
660
|
+
return DEFAULT_IGNORE_PATTERNS;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
698
663
|
Object.defineProperty(exports, 'DEFAULT_VERSION', {
|
|
699
664
|
enumerable: true,
|
|
700
665
|
get: function () {
|
|
@@ -725,9 +690,63 @@ Object.defineProperty(exports, '__toESM', {
|
|
|
725
690
|
return __toESM;
|
|
726
691
|
}
|
|
727
692
|
});
|
|
728
|
-
Object.defineProperty(exports, '
|
|
693
|
+
Object.defineProperty(exports, 'applyCodeActions', {
|
|
694
|
+
enumerable: true,
|
|
695
|
+
get: function () {
|
|
696
|
+
return applyCodeActions;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
Object.defineProperty(exports, 'createState', {
|
|
700
|
+
enumerable: true,
|
|
701
|
+
get: function () {
|
|
702
|
+
return createState;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
Object.defineProperty(exports, 'fileExists', {
|
|
706
|
+
enumerable: true,
|
|
707
|
+
get: function () {
|
|
708
|
+
return fileExists;
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
Object.defineProperty(exports, 'findTailwindConfigPath', {
|
|
712
|
+
enumerable: true,
|
|
713
|
+
get: function () {
|
|
714
|
+
return findTailwindConfigPath;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
Object.defineProperty(exports, 'getLanguageId', {
|
|
718
|
+
enumerable: true,
|
|
719
|
+
get: function () {
|
|
720
|
+
return getLanguageId;
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
Object.defineProperty(exports, 'isCssConfigFile', {
|
|
724
|
+
enumerable: true,
|
|
725
|
+
get: function () {
|
|
726
|
+
return isCssConfigFile;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
Object.defineProperty(exports, 'loadTailwindConfig', {
|
|
730
|
+
enumerable: true,
|
|
731
|
+
get: function () {
|
|
732
|
+
return loadTailwindConfig;
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
Object.defineProperty(exports, 'readFileSync', {
|
|
736
|
+
enumerable: true,
|
|
737
|
+
get: function () {
|
|
738
|
+
return readFileSync;
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
Object.defineProperty(exports, 'readGitignorePatterns', {
|
|
742
|
+
enumerable: true,
|
|
743
|
+
get: function () {
|
|
744
|
+
return readGitignorePatterns;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
Object.defineProperty(exports, 'writeFileSync', {
|
|
729
748
|
enumerable: true,
|
|
730
749
|
get: function () {
|
|
731
|
-
return
|
|
750
|
+
return writeFileSync;
|
|
732
751
|
}
|
|
733
752
|
});
|
package/package.json
CHANGED
|
@@ -1,74 +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
|
-
|
|
2
|
+
"name": "tailwind-lint",
|
|
3
|
+
"version": "0.8.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
|
+
"chalk": "^5.6.2",
|
|
63
|
+
"commander": "^14.0.3",
|
|
64
|
+
"fast-glob": "^3.3.3",
|
|
65
|
+
"postcss": "^8.5.6",
|
|
66
|
+
"vscode-languageserver-textdocument": "^1.0.12"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "^25.3.0",
|
|
70
|
+
"oxfmt": "^0.34.0",
|
|
71
|
+
"oxlint": "^1.49.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.1"
|
|
81
|
+
}
|