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 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-BHl8x2Q1.cjs');
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 chalk = require("chalk");
7
- chalk = require_state.__toESM(chalk);
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/cli.ts
13
- const MAX_FILENAME_DISPLAY_LENGTH = 50;
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(chalk.default.underline.bold(file.path));
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(chalk.default.green(` ✔ Fixed ${issueText}`));
123
+ console.log(ansis.default.green(` ✔ Fixed ${issueText}`));
67
124
  totalFixed += file.fixedCount || 0;
68
125
  }
69
- const { errors, warnings } = countDiagnosticsBySeverity(file.diagnostics);
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 ? chalk.default.red(severity) : chalk.default.yellow(severity);
78
- const code = diagnostic.code ? chalk.default.dim(` (${diagnostic.code})`) : "";
79
- console.log(` ${chalk.default.dim(`${line}:${char}`)} ${severityColor} ${diagnostic.message}${code}`);
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(chalk.default.green.bold(`✔ Fixed ${issueText}`));
86
- } else console.log(chalk.default.green.bold("✔ No issues found"));
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(chalk.default.green.bold(`✔ Fixed ${issueText}`));
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 += `${chalk.default.bold.cyan("Usage:")} ${helper.commandUsage(cmd)}\n\n`;
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 += `${chalk.default.bold.cyan("Arguments:")}\n`;
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 += ` ${chalk.default.green(argName.padEnd(termWidth))} ${arg.description}\n`;
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 += `${chalk.default.bold.cyan("Options:")}\n`;
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 += ` ${chalk.default.yellow(flags.padEnd(termWidth))} ${description}\n`;
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
- ${chalk.default.bold.cyan("Examples:")}
137
- ${chalk.default.dim("$")} tailwind-lint
138
- ${chalk.default.dim("$")} tailwind-lint ${chalk.default.green("\"src/**/*.{js,jsx,ts,tsx}\"")}
139
- ${chalk.default.dim("$")} tailwind-lint ${chalk.default.yellow("--config")} ${chalk.default.green("./tailwind.config.js")}
140
- ${chalk.default.dim("$")} tailwind-lint ${chalk.default.green("\"src/**/*.tsx\"")} ${chalk.default.yellow("--fix")}
141
- ${chalk.default.dim("$")} tailwind-lint ${chalk.default.green("\"**/*.vue\"")}
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
- ${chalk.default.bold.cyan("Notes:")}
144
- ${chalk.default.dim("•")} Running without arguments auto-discovers config and files
145
- ${chalk.default.dim("•")} For v3: uses content patterns from tailwind.config.js
146
- ${chalk.default.dim("•")} For v4: uses @source patterns from CSS config, or default pattern
147
- ${chalk.default.dim("•")} Default pattern: ${chalk.default.dim("./**/*.{js,jsx,ts,tsx,html}")}
148
- ${chalk.default.dim("•")} Use ${chalk.default.yellow("--fix")} to automatically resolve fixable issues
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(chalk.default.cyan.bold("→ Configuration"));
157
- console.log(chalk.default.dim(` Working directory: ${resolved.cwd}`));
158
- console.log(chalk.default.dim(` Config path: ${resolved.configPath || "auto-discover"}`));
159
- console.log(chalk.default.dim(` Fix mode: ${resolved.fix}`));
160
- console.log(chalk.default.dim(` Patterns: ${resolved.patterns.length > 0 ? resolved.patterns.join(", ") : "auto-discover"}`));
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${chalk.default.cyan("→")} Linting files... ${chalk.default.dim(`(${current}/${total})`)} ${chalk.default.dim(displayFile)}${" ".repeat(require_state.TERMINAL_PADDING)}`);
169
- } else if (resolved.verbose) console.log(chalk.default.dim(` [${current}/${total}] Linting ${file}`));
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
- console.log(chalk.default.yellow("⚠ No files found to lint"));
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(chalk.default.green.bold("✔ No issues found"));
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
- await displayResults(results.files, resolved.fix);
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.error(chalk.default.red("✖ Error:"), errorMessage);
188
- if (resolved.verbose && error instanceof Error) {
189
- console.error(chalk.default.dim("\nStack trace:"));
190
- console.error(chalk.default.dim(error.stack || error.toString()));
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-BHl8x2Q1.cjs');
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 chalk = require("chalk");
7
- chalk = require_state.__toESM(chalk);
8
- let fast_glob = require("fast-glob");
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
- return (0, fast_glob.default)(patterns, {
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 { include, exclude } = extractSourcePatterns(require_state.readFileSync(configFilePath));
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(chalk.default.cyan.bold(`→ Discovered ${files.length} file${files.length !== 1 ? "s" : ""} to lint`));
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 chalk = require("chalk");
32
- chalk = __toESM(chalk);
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(chalk.default.dim(" ✓ Loaded v3 JIT modules"));
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(chalk.default.yellow(` ⚠ Warning: Could not load v3 JIT modules: ${message}`));
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(chalk.default.dim(" ✓ Created JIT context"));
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(chalk.default.yellow(` ⚠ Warning: Could not create JIT context: ${message}`));
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(chalk.default.dim(" ✓ Loaded v4 design system"));
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(chalk.default.cyan.bold("→ Tailwind Configuration"));
555
- console.log(chalk.default.dim(` Version: ${version || "unknown"}`));
556
- console.log(chalk.default.dim(` Config type: ${isCssConfig ? "CSS (v4)" : "JavaScript"}`));
557
- console.log(chalk.default.dim(` Config path: ${resolvedConfigPath}`));
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
- "name": "tailwind-lint",
3
- "version": "0.7.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
- "dependencies": {
46
- "@tailwindcss/language-service": "^0.14.29",
47
- "chalk": "^5.6.2",
48
- "commander": "^14.0.3",
49
- "fast-glob": "^3.3.3",
50
- "postcss": "^8.5.6",
51
- "vscode-languageserver-textdocument": "^1.0.12"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^25.2.2",
55
- "oxfmt": "^0.28.0",
56
- "oxlint": "^1.43.0",
57
- "tsdown": "^0.20.3",
58
- "typescript": "^5.9.3",
59
- "vitest": "^4.0.18"
60
- },
61
- "engines": {
62
- "node": ">=22.0.0",
63
- "pnpm": ">=10.0.0"
64
- },
65
- "scripts": {
66
- "build": "tsdown",
67
- "dev": "tsdown --watch",
68
- "format": "oxfmt --write .",
69
- "format:check": "oxfmt --check .",
70
- "lint": "oxlint .",
71
- "lint:fix": "oxlint --fix .",
72
- "start": "node dist/cli.cjs",
73
- "test": "vitest run",
74
- "test:coverage": "vitest run --coverage",
75
- "test:watch": "vitest"
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
+ }