tailwind-lint 0.5.1 → 0.7.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 +16 -10
- package/dist/cli.cjs +15 -14
- package/dist/linter.cjs +144 -2
- package/dist/linter.d.cts +5 -1
- package/dist/{linter-B1HK1nl2.cjs → state-BHl8x2Q1.cjs} +116 -127
- package/package.json +26 -22
package/README.md
CHANGED
|
@@ -48,11 +48,15 @@ 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.
|
|
53
|
+
- Searches in project root and subdirectories: `./`, `./src/`, `./src/styles/`, `./app/`, etc.
|
|
52
54
|
- Uses file patterns from `@source` directives if present
|
|
53
|
-
- Falls back to default pattern: `./**/*.{js,jsx,ts,tsx,html}`
|
|
55
|
+
- Falls back to default pattern: `./**/*.{js,jsx,ts,tsx,html,vue,svelte,astro,mdx}`
|
|
56
|
+
- **Note:** When CSS config is in a subdirectory (e.g., `src/styles/global.css`), files are discovered from the project root
|
|
54
57
|
|
|
55
58
|
**Tailwind CSS v3:**
|
|
59
|
+
|
|
56
60
|
- Finds JavaScript config files: `tailwind.config.js`, `tailwind.config.cjs`, `tailwind.config.mjs`, `tailwind.config.ts`
|
|
57
61
|
- Uses file patterns from the `content` array in your config
|
|
58
62
|
|
|
@@ -91,7 +95,7 @@ Create a CSS config file (`app.css`, `index.css`, or `tailwind.css`):
|
|
|
91
95
|
@import "tailwindcss";
|
|
92
96
|
|
|
93
97
|
@theme {
|
|
94
|
-
|
|
98
|
+
--color-primary: #3b82f6;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
@source "./src/**/*.{js,jsx,ts,tsx,html}";
|
|
@@ -106,12 +110,12 @@ Create a JavaScript config file (`tailwind.config.js`):
|
|
|
106
110
|
```javascript
|
|
107
111
|
/** @type {import('tailwindcss').Config} */
|
|
108
112
|
module.exports = {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
113
|
+
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
114
|
+
theme: {
|
|
115
|
+
extend: {},
|
|
116
|
+
},
|
|
117
|
+
plugins: [],
|
|
118
|
+
};
|
|
115
119
|
```
|
|
116
120
|
|
|
117
121
|
## Autofix
|
|
@@ -126,7 +130,8 @@ Files are written atomically with multiple iterations to ensure all fixes are ap
|
|
|
126
130
|
## Features
|
|
127
131
|
|
|
128
132
|
**Core (v3 & v4):**
|
|
129
|
-
|
|
133
|
+
|
|
134
|
+
- 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
|
|
130
135
|
- Invalid @apply Usage - Validates if a class can be used with `@apply`
|
|
131
136
|
- Invalid @screen References - Detects references to non-existent breakpoints
|
|
132
137
|
- Invalid Config Paths - Validates references in `config()` and `theme()` functions
|
|
@@ -136,6 +141,7 @@ Files are written atomically with multiple iterations to ensure all fixes are ap
|
|
|
136
141
|
- Autofix - Automatically fix issues with `--fix` flag
|
|
137
142
|
|
|
138
143
|
**v4-Specific:**
|
|
144
|
+
|
|
139
145
|
- Canonical Class Suggestions - Suggests shorthand equivalents for arbitrary values (e.g., `top-[60px]` → `top-15`)
|
|
140
146
|
- Invalid @source Directives - Validates `@source` directive paths
|
|
141
147
|
- Full theme loading - Automatically loads Tailwind's default theme
|
package/dist/cli.cjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
2
|
+
const require_state = require('./state-BHl8x2Q1.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
12
|
//#region src/cli.ts
|
|
@@ -14,8 +15,8 @@ function countDiagnosticsBySeverity(diagnostics) {
|
|
|
14
15
|
let errors = 0;
|
|
15
16
|
let warnings = 0;
|
|
16
17
|
for (const diagnostic of diagnostics) {
|
|
17
|
-
if (diagnostic.severity ===
|
|
18
|
-
if (diagnostic.severity ===
|
|
18
|
+
if (diagnostic.severity === require_state.SEVERITY.ERROR) errors++;
|
|
19
|
+
if (diagnostic.severity === require_state.SEVERITY.WARNING) warnings++;
|
|
19
20
|
}
|
|
20
21
|
return {
|
|
21
22
|
errors,
|
|
@@ -33,7 +34,7 @@ function resolveOptions(files, options) {
|
|
|
33
34
|
const absoluteConfigPath = node_path.isAbsolute(options.config) ? options.config : node_path.resolve(process.cwd(), options.config);
|
|
34
35
|
cwd = node_path.dirname(absoluteConfigPath);
|
|
35
36
|
configPath = node_path.basename(absoluteConfigPath);
|
|
36
|
-
patterns = [
|
|
37
|
+
patterns = [require_state.DEFAULT_FILE_PATTERN];
|
|
37
38
|
}
|
|
38
39
|
const autoDiscover = hasAutoFlag;
|
|
39
40
|
return {
|
|
@@ -72,8 +73,8 @@ async function displayResults(files, fixMode) {
|
|
|
72
73
|
for (const diagnostic of file.diagnostics) {
|
|
73
74
|
const line = diagnostic.range.start.line + 1;
|
|
74
75
|
const char = diagnostic.range.start.character + 1;
|
|
75
|
-
const severity = diagnostic.severity ===
|
|
76
|
-
const severityColor = diagnostic.severity ===
|
|
76
|
+
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);
|
|
77
78
|
const code = diagnostic.code ? chalk.default.dim(` (${diagnostic.code})`) : "";
|
|
78
79
|
console.log(` ${chalk.default.dim(`${line}:${char}`)} ${severityColor} ${diagnostic.message}${code}`);
|
|
79
80
|
}
|
|
@@ -100,9 +101,9 @@ const program = new commander.Command();
|
|
|
100
101
|
const getVersion = () => {
|
|
101
102
|
const packageJsonPath = node_path.join(__dirname, "../package.json");
|
|
102
103
|
try {
|
|
103
|
-
return JSON.parse(node_fs.readFileSync(packageJsonPath, "utf-8")).version ||
|
|
104
|
+
return JSON.parse(node_fs.readFileSync(packageJsonPath, "utf-8")).version || require_state.DEFAULT_VERSION;
|
|
104
105
|
} catch {
|
|
105
|
-
return
|
|
106
|
+
return require_state.DEFAULT_VERSION;
|
|
106
107
|
}
|
|
107
108
|
};
|
|
108
109
|
program.configureHelp({ formatHelp: (cmd, helper) => {
|
|
@@ -164,11 +165,11 @@ ${chalk.default.bold.cyan("Notes:")}
|
|
|
164
165
|
onProgress: (current, total, file) => {
|
|
165
166
|
if (process.stdout.isTTY && !resolved.verbose) {
|
|
166
167
|
const displayFile = truncateFilename(file);
|
|
167
|
-
process.stdout.write(`\r${chalk.default.cyan("→")} Linting files... ${chalk.default.dim(`(${current}/${total})`)} ${chalk.default.dim(displayFile)}${" ".repeat(
|
|
168
|
+
process.stdout.write(`\r${chalk.default.cyan("→")} Linting files... ${chalk.default.dim(`(${current}/${total})`)} ${chalk.default.dim(displayFile)}${" ".repeat(require_state.TERMINAL_PADDING)}`);
|
|
168
169
|
} else if (resolved.verbose) console.log(chalk.default.dim(` [${current}/${total}] Linting ${file}`));
|
|
169
170
|
}
|
|
170
171
|
});
|
|
171
|
-
if (process.stdout.isTTY && !resolved.verbose) process.stdout.write(`\r${" ".repeat(
|
|
172
|
+
if (process.stdout.isTTY && !resolved.verbose) process.stdout.write(`\r${" ".repeat(require_state.TERMINAL_WIDTH)}\r`);
|
|
172
173
|
if (results.totalFilesProcessed === 0) {
|
|
173
174
|
console.log();
|
|
174
175
|
console.log(chalk.default.yellow("⚠ No files found to lint"));
|
|
@@ -179,7 +180,7 @@ ${chalk.default.bold.cyan("Notes:")}
|
|
|
179
180
|
process.exit(0);
|
|
180
181
|
}
|
|
181
182
|
await displayResults(results.files, resolved.fix);
|
|
182
|
-
const hasErrors = results.files.some((file) => file.diagnostics.some((d) => d.severity ===
|
|
183
|
+
const hasErrors = results.files.some((file) => file.diagnostics.some((d) => d.severity === require_state.SEVERITY.ERROR));
|
|
183
184
|
process.exit(hasErrors ? 1 : 0);
|
|
184
185
|
} catch (error) {
|
|
185
186
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
package/dist/linter.cjs
CHANGED
|
@@ -1,3 +1,145 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_state = require('./state-BHl8x2Q1.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
|
+
return (0, fast_glob.default)(patterns, {
|
|
37
|
+
cwd,
|
|
38
|
+
absolute: false,
|
|
39
|
+
ignore: [...require_state.DEFAULT_IGNORE_PATTERNS, ...extraIgnore]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function discoverFilesFromConfig(cwd, configPath) {
|
|
43
|
+
const configFilePath = await require_state.findTailwindConfigPath(cwd, configPath);
|
|
44
|
+
if (!configFilePath) throw new Error("Could not find Tailwind config for auto-discovery.\nUse --config to specify the path, or provide file patterns directly.");
|
|
45
|
+
if (!require_state.isCssConfigFile(configFilePath)) {
|
|
46
|
+
const config = await require_state.loadTailwindConfig(configFilePath);
|
|
47
|
+
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}']");
|
|
48
|
+
const patterns = extractContentPatterns(config);
|
|
49
|
+
if (patterns.length === 0) throw new Error("No content patterns found in Tailwind config.\nEnsure your config has a content array with file patterns.");
|
|
50
|
+
return expandPatterns(cwd, patterns);
|
|
51
|
+
}
|
|
52
|
+
const configDir = node_path.dirname(configFilePath);
|
|
53
|
+
const { include, exclude } = extractSourcePatterns(require_state.readFileSync(configFilePath));
|
|
54
|
+
const resolveFromConfig = (pattern) => {
|
|
55
|
+
const absolutePattern = node_path.resolve(configDir, pattern);
|
|
56
|
+
return node_path.relative(cwd, absolutePattern);
|
|
57
|
+
};
|
|
58
|
+
const resolvedExclude = exclude.map(resolveFromConfig);
|
|
59
|
+
const gitignorePatterns = require_state.readGitignorePatterns(cwd);
|
|
60
|
+
const extraIgnore = [...resolvedExclude, ...gitignorePatterns];
|
|
61
|
+
if (include.length > 0) return expandPatterns(cwd, include.map(resolveFromConfig), extraIgnore);
|
|
62
|
+
return expandPatterns(cwd, [require_state.DEFAULT_FILE_PATTERN], extraIgnore);
|
|
63
|
+
}
|
|
64
|
+
function extractContentPatterns(config) {
|
|
65
|
+
if (!config.content) return [];
|
|
66
|
+
return (Array.isArray(config.content) ? config.content : config.content.files || []).filter((p) => typeof p === "string");
|
|
67
|
+
}
|
|
68
|
+
function extractSourcePatterns(cssContent) {
|
|
69
|
+
const include = [];
|
|
70
|
+
const exclude = [];
|
|
71
|
+
for (const match of cssContent.matchAll(/@source\s+(not\s+)?(?:inline\(|["']([^"']+)["'])/g)) {
|
|
72
|
+
const isNot = !!match[1];
|
|
73
|
+
const filePath = match[2];
|
|
74
|
+
if (!filePath) continue;
|
|
75
|
+
if (isNot) exclude.push(filePath);
|
|
76
|
+
else include.push(filePath);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
include,
|
|
80
|
+
exclude
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function processFiles(state, cwd, files, fix, onProgress) {
|
|
84
|
+
const results = [];
|
|
85
|
+
for (let i = 0; i < files.length; i += require_state.CONCURRENT_FILES) {
|
|
86
|
+
const batch = files.slice(i, i + require_state.CONCURRENT_FILES);
|
|
87
|
+
const batchResults = await Promise.all(batch.map(async (file, batchIndex) => {
|
|
88
|
+
if (onProgress) onProgress(i + batchIndex + 1, files.length, file);
|
|
89
|
+
return processFile(state, cwd, file, fix);
|
|
90
|
+
}));
|
|
91
|
+
results.push(...batchResults.filter((r) => r !== null));
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
async function processFile(state, cwd, filePath, fix) {
|
|
96
|
+
const absolutePath = node_path.isAbsolute(filePath) ? filePath : node_path.resolve(cwd, filePath);
|
|
97
|
+
if (!require_state.fileExists(absolutePath)) return null;
|
|
98
|
+
const content = require_state.readFileSync(absolutePath);
|
|
99
|
+
let diagnostics = await validateDocument(state, absolutePath, content);
|
|
100
|
+
let fixedCount = 0;
|
|
101
|
+
if (fix && diagnostics.length > 0) {
|
|
102
|
+
const fixResult = await require_state.applyCodeActions(state, absolutePath, content, diagnostics);
|
|
103
|
+
if (fixResult.changed) {
|
|
104
|
+
require_state.writeFileSync(absolutePath, fixResult.content);
|
|
105
|
+
fixedCount = fixResult.fixedCount;
|
|
106
|
+
diagnostics = await validateDocument(state, absolutePath, fixResult.content);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
path: node_path.relative(cwd, absolutePath),
|
|
111
|
+
diagnostics,
|
|
112
|
+
fixed: fixedCount > 0,
|
|
113
|
+
fixedCount
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function initializeState(cwd, configPath, verbose = false) {
|
|
117
|
+
try {
|
|
118
|
+
const state = await require_state.createState(cwd, configPath, verbose);
|
|
119
|
+
if (verbose) console.log();
|
|
120
|
+
return state;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
throw new Error(`Failed to initialize Tailwind state: ${message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verbose = false, onProgress }) {
|
|
127
|
+
const state = await initializeState(cwd, configPath, verbose);
|
|
128
|
+
const files = await discoverFiles(cwd, patterns, configPath, autoDiscover);
|
|
129
|
+
if (verbose) {
|
|
130
|
+
console.log(chalk.default.cyan.bold(`→ Discovered ${files.length} file${files.length !== 1 ? "s" : ""} to lint`));
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
if (files.length === 0) return {
|
|
134
|
+
files: [],
|
|
135
|
+
totalFilesProcessed: 0
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
files: (await processFiles(state, cwd, files, fix, onProgress)).filter((result) => result.diagnostics.length > 0 || result.fixed),
|
|
139
|
+
totalFilesProcessed: files.length
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
exports.extractSourcePatterns = extractSourcePatterns;
|
|
145
|
+
exports.lint = lint;
|
package/dist/linter.d.cts
CHANGED
|
@@ -22,6 +22,10 @@ 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
|
+
};
|
|
25
29
|
declare function lint({
|
|
26
30
|
cwd,
|
|
27
31
|
patterns,
|
|
@@ -32,4 +36,4 @@ declare function lint({
|
|
|
32
36
|
onProgress
|
|
33
37
|
}: LintOptions): Promise<LintResult>;
|
|
34
38
|
//#endregion
|
|
35
|
-
export { type LintFileResult, type LintOptions, type LintResult, lint };
|
|
39
|
+
export { type LintFileResult, type LintOptions, type LintResult, 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;
|
|
@@ -30,12 +30,12 @@ node_path = __toESM(node_path);
|
|
|
30
30
|
let _tailwindcss_language_service = require("@tailwindcss/language-service");
|
|
31
31
|
let chalk = require("chalk");
|
|
32
32
|
chalk = __toESM(chalk);
|
|
33
|
-
let fast_glob = require("fast-glob");
|
|
34
|
-
fast_glob = __toESM(fast_glob);
|
|
35
33
|
let vscode_languageserver_textdocument = require("vscode-languageserver-textdocument");
|
|
36
34
|
let node_module = require("node:module");
|
|
37
35
|
let node_fs = require("node:fs");
|
|
38
36
|
node_fs = __toESM(node_fs);
|
|
37
|
+
let postcss = require("postcss");
|
|
38
|
+
postcss = __toESM(postcss);
|
|
39
39
|
|
|
40
40
|
//#region src/constants.ts
|
|
41
41
|
const DEFAULT_IGNORE_PATTERNS = [
|
|
@@ -51,7 +51,7 @@ const DEFAULT_IGNORE_PATTERNS = [
|
|
|
51
51
|
"**/.cache/**",
|
|
52
52
|
"**/.DS_Store/**"
|
|
53
53
|
];
|
|
54
|
-
const DEFAULT_FILE_PATTERN = "./**/*.{js,jsx,ts,tsx,html}";
|
|
54
|
+
const DEFAULT_FILE_PATTERN = "./**/*.{js,jsx,ts,tsx,html,vue,svelte,astro,mdx}";
|
|
55
55
|
const V3_CONFIG_PATHS = [
|
|
56
56
|
"tailwind.config.js",
|
|
57
57
|
"tailwind.config.cjs",
|
|
@@ -66,11 +66,13 @@ const V4_CSS_NAMES = [
|
|
|
66
66
|
"global.css",
|
|
67
67
|
"styles.css",
|
|
68
68
|
"style.css",
|
|
69
|
-
"main.css"
|
|
69
|
+
"main.css",
|
|
70
|
+
"input.css"
|
|
70
71
|
];
|
|
71
72
|
const V4_CSS_FOLDERS = [
|
|
72
73
|
"./",
|
|
73
74
|
"./src/",
|
|
75
|
+
"./src/app/",
|
|
74
76
|
"./src/css/",
|
|
75
77
|
"./src/style/",
|
|
76
78
|
"./src/styles/",
|
|
@@ -78,9 +80,12 @@ const V4_CSS_FOLDERS = [
|
|
|
78
80
|
"./app/css/",
|
|
79
81
|
"./app/style/",
|
|
80
82
|
"./app/styles/",
|
|
83
|
+
"./app/assets/css/",
|
|
81
84
|
"./css/",
|
|
82
85
|
"./style/",
|
|
83
|
-
"./styles/"
|
|
86
|
+
"./styles/",
|
|
87
|
+
"./assets/css/",
|
|
88
|
+
"./resources/css/"
|
|
84
89
|
];
|
|
85
90
|
const LANGUAGE_MAP = {
|
|
86
91
|
".astro": "astro",
|
|
@@ -116,6 +121,7 @@ const TERMINAL_WIDTH = 80;
|
|
|
116
121
|
const TERMINAL_PADDING = 10;
|
|
117
122
|
const MAX_FIX_ITERATIONS = 100;
|
|
118
123
|
const QUICKFIX_ACTION_KIND = "quickfix";
|
|
124
|
+
const TAILWIND_V4_IMPORT_REGEX = /@import\s+["']tailwindcss(?:["'\s/]|$)/;
|
|
119
125
|
function getLanguageId(filePath) {
|
|
120
126
|
return LANGUAGE_MAP[filePath.substring(filePath.lastIndexOf(".")).toLowerCase()] || "html";
|
|
121
127
|
}
|
|
@@ -216,6 +222,7 @@ function createEditorState(cwd) {
|
|
|
216
222
|
const settings = {
|
|
217
223
|
editor: { tabSize: DEFAULT_TAB_SIZE },
|
|
218
224
|
tailwindCSS: {
|
|
225
|
+
validate: true,
|
|
219
226
|
inspectPort: null,
|
|
220
227
|
emmetCompletions: false,
|
|
221
228
|
includeLanguages: {},
|
|
@@ -235,7 +242,6 @@ function createEditorState(cwd) {
|
|
|
235
242
|
hovers: true,
|
|
236
243
|
codeLens: false,
|
|
237
244
|
suggestions: true,
|
|
238
|
-
validate: true,
|
|
239
245
|
colorDecorators: true,
|
|
240
246
|
rootFontSize: DEFAULT_ROOT_FONT_SIZE,
|
|
241
247
|
showPixelEquivalents: true,
|
|
@@ -332,6 +338,19 @@ async function loadV3ClassMetadata(state, cwd, verbose = false) {
|
|
|
332
338
|
} };
|
|
333
339
|
}
|
|
334
340
|
extractConfigMetadata(state);
|
|
341
|
+
if (!state.classNames) state.classNames = {
|
|
342
|
+
context: {},
|
|
343
|
+
classNames: {}
|
|
344
|
+
};
|
|
345
|
+
if (state.modules?.jit?.createContext && state.config) try {
|
|
346
|
+
state.jitContext = state.modules.jit.createContext.module(state.config);
|
|
347
|
+
if (verbose) console.log(chalk.default.dim(" ✓ Created JIT context"));
|
|
348
|
+
} catch (contextError) {
|
|
349
|
+
if (verbose) {
|
|
350
|
+
const message = contextError instanceof Error ? contextError.message : String(contextError);
|
|
351
|
+
console.log(chalk.default.yellow(` ⚠ Warning: Could not create JIT context: ${message}`));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
335
354
|
} catch (error) {
|
|
336
355
|
if (error instanceof Error) throw new AdapterLoadError("v3", error);
|
|
337
356
|
throw new Error(`Failed to load v3 class metadata: ${String(error)}`);
|
|
@@ -371,6 +390,19 @@ function writeFileSync(filePath, content) {
|
|
|
371
390
|
if (typeof content !== "string") throw new TypeError("Content must be a string");
|
|
372
391
|
node_fs.writeFileSync(filePath, content, "utf-8");
|
|
373
392
|
}
|
|
393
|
+
function readGitignorePatterns(cwd) {
|
|
394
|
+
const gitignorePath = node_path.join(cwd, ".gitignore");
|
|
395
|
+
if (!fileExists(gitignorePath)) return [];
|
|
396
|
+
try {
|
|
397
|
+
return node_fs.readFileSync(gitignorePath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!")).map((pattern) => {
|
|
398
|
+
const cleaned = pattern.replace(/\/+$/, "");
|
|
399
|
+
if (cleaned.includes("/") || cleaned.includes("*")) return cleaned.endsWith("/**") ? cleaned : `${cleaned}/**`;
|
|
400
|
+
return `**/${cleaned}/**`;
|
|
401
|
+
});
|
|
402
|
+
} catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
374
406
|
|
|
375
407
|
//#endregion
|
|
376
408
|
//#region src/adapters/v4-adapter.ts
|
|
@@ -426,11 +458,15 @@ async function loadV4DesignSystem(state, cwd, configPath, verbose = false) {
|
|
|
426
458
|
Object.assign(designSystem, {
|
|
427
459
|
dependencies: () => /* @__PURE__ */ new Set(),
|
|
428
460
|
compile(classes) {
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
461
|
+
if (designSystem.candidatesToCss) return designSystem.candidatesToCss(classes).map((result) => {
|
|
462
|
+
if (typeof result === "string" && result.length > 0) try {
|
|
463
|
+
return postcss.default.parse(result);
|
|
464
|
+
} catch {
|
|
465
|
+
return postcss.default.root();
|
|
466
|
+
}
|
|
467
|
+
return postcss.default.root();
|
|
433
468
|
});
|
|
469
|
+
return classes.map(() => postcss.default.root());
|
|
434
470
|
}
|
|
435
471
|
});
|
|
436
472
|
state.designSystem = designSystem;
|
|
@@ -478,7 +514,7 @@ async function findTailwindConfigPath(cwd, configPath) {
|
|
|
478
514
|
const fullPath = node_path.join(cwd, p);
|
|
479
515
|
if (fileExists(fullPath)) try {
|
|
480
516
|
const content = readFileSync(fullPath);
|
|
481
|
-
if (
|
|
517
|
+
if (TAILWIND_V4_IMPORT_REGEX.test(content)) return fullPath;
|
|
482
518
|
} catch {}
|
|
483
519
|
}
|
|
484
520
|
return null;
|
|
@@ -552,125 +588,24 @@ async function createState(cwd, configPath, verbose = false) {
|
|
|
552
588
|
}
|
|
553
589
|
|
|
554
590
|
//#endregion
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const languageId = getLanguageId(filePath);
|
|
562
|
-
const uri = `file://${filePath}`;
|
|
563
|
-
return await (0, _tailwindcss_language_service.doValidate)(state, vscode_languageserver_textdocument.TextDocument.create(uri, languageId, 1, content));
|
|
564
|
-
} catch (error) {
|
|
565
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
566
|
-
if (message.includes("Cannot read") || message.includes("undefined")) {
|
|
567
|
-
console.warn(`Warning: Language service crashed while validating ${filePath}. Skipping this file.`);
|
|
568
|
-
return [];
|
|
569
|
-
}
|
|
570
|
-
throw new Error(`Failed to validate document ${filePath}: ${message}`);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
async function discoverFiles(cwd, patterns, configPath, autoDiscover) {
|
|
574
|
-
if (autoDiscover) return discoverFilesFromConfig(cwd, configPath);
|
|
575
|
-
return expandPatterns(cwd, patterns);
|
|
576
|
-
}
|
|
577
|
-
async function expandPatterns(cwd, patterns) {
|
|
578
|
-
return (0, fast_glob.default)(patterns, {
|
|
579
|
-
cwd,
|
|
580
|
-
absolute: false,
|
|
581
|
-
ignore: DEFAULT_IGNORE_PATTERNS
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
async function discoverFilesFromConfig(cwd, configPath) {
|
|
585
|
-
const configFilePath = await findTailwindConfigPath(cwd, configPath);
|
|
586
|
-
if (!configFilePath) throw new Error("Could not find Tailwind config for auto-discovery.\nUse --config to specify the path, or provide file patterns directly.");
|
|
587
|
-
if (!isCssConfigFile(configFilePath)) {
|
|
588
|
-
const config = await loadTailwindConfig(configFilePath);
|
|
589
|
-
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}']");
|
|
590
|
-
const patterns = extractContentPatterns(config);
|
|
591
|
-
if (patterns.length === 0) throw new Error("No content patterns found in Tailwind config.\nEnsure your config has a content array with file patterns.");
|
|
592
|
-
return expandPatterns(cwd, patterns);
|
|
593
|
-
}
|
|
594
|
-
const sourcePatterns = extractSourcePatterns(readFileSync(configFilePath));
|
|
595
|
-
if (sourcePatterns.length > 0) return expandPatterns(cwd, sourcePatterns);
|
|
596
|
-
return expandPatterns(cwd, [DEFAULT_FILE_PATTERN]);
|
|
597
|
-
}
|
|
598
|
-
function extractContentPatterns(config) {
|
|
599
|
-
if (!config.content) return [];
|
|
600
|
-
return (Array.isArray(config.content) ? config.content : config.content.files || []).filter((p) => typeof p === "string");
|
|
601
|
-
}
|
|
602
|
-
function extractSourcePatterns(cssContent) {
|
|
603
|
-
const patterns = [];
|
|
604
|
-
for (const match of cssContent.matchAll(/@source\s+["']([^"']+)["']/g)) patterns.push(match[1]);
|
|
605
|
-
return patterns;
|
|
606
|
-
}
|
|
607
|
-
async function processFiles(state, cwd, files, fix, onProgress) {
|
|
608
|
-
const results = [];
|
|
609
|
-
for (let i = 0; i < files.length; i += CONCURRENT_FILES) {
|
|
610
|
-
const batch = files.slice(i, i + CONCURRENT_FILES);
|
|
611
|
-
const batchResults = await Promise.all(batch.map(async (file, batchIndex) => {
|
|
612
|
-
if (onProgress) onProgress(i + batchIndex + 1, files.length, file);
|
|
613
|
-
return processFile(state, cwd, file, fix);
|
|
614
|
-
}));
|
|
615
|
-
results.push(...batchResults.filter((r) => r !== null));
|
|
616
|
-
}
|
|
617
|
-
return results;
|
|
618
|
-
}
|
|
619
|
-
async function processFile(state, cwd, filePath, fix) {
|
|
620
|
-
const absolutePath = node_path.isAbsolute(filePath) ? filePath : node_path.resolve(cwd, filePath);
|
|
621
|
-
if (!fileExists(absolutePath)) return null;
|
|
622
|
-
const content = readFileSync(absolutePath);
|
|
623
|
-
let diagnostics = await validateDocument(state, absolutePath, content);
|
|
624
|
-
let fixedCount = 0;
|
|
625
|
-
if (fix && diagnostics.length > 0) {
|
|
626
|
-
const fixResult = await applyCodeActions(state, absolutePath, content, diagnostics);
|
|
627
|
-
if (fixResult.changed) {
|
|
628
|
-
writeFileSync(absolutePath, fixResult.content);
|
|
629
|
-
fixedCount = fixResult.fixedCount;
|
|
630
|
-
diagnostics = await validateDocument(state, absolutePath, fixResult.content);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return {
|
|
634
|
-
path: node_path.relative(cwd, absolutePath),
|
|
635
|
-
diagnostics,
|
|
636
|
-
fixed: fixedCount > 0,
|
|
637
|
-
fixedCount
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
async function initializeState(cwd, configPath, verbose = false) {
|
|
641
|
-
try {
|
|
642
|
-
const state = await createState(cwd, configPath, verbose);
|
|
643
|
-
if (verbose) console.log();
|
|
644
|
-
return state;
|
|
645
|
-
} catch (error) {
|
|
646
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
647
|
-
throw new Error(`Failed to initialize Tailwind state: ${message}`);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verbose = false, onProgress }) {
|
|
651
|
-
const state = await initializeState(cwd, configPath, verbose);
|
|
652
|
-
const files = await discoverFiles(cwd, patterns, configPath, autoDiscover);
|
|
653
|
-
if (verbose) {
|
|
654
|
-
console.log(chalk.default.cyan.bold(`→ Discovered ${files.length} file${files.length !== 1 ? "s" : ""} to lint`));
|
|
655
|
-
console.log();
|
|
656
|
-
}
|
|
657
|
-
if (files.length === 0) return {
|
|
658
|
-
files: [],
|
|
659
|
-
totalFilesProcessed: 0
|
|
660
|
-
};
|
|
661
|
-
return {
|
|
662
|
-
files: (await processFiles(state, cwd, files, fix, onProgress)).filter((result) => result.diagnostics.length > 0 || result.fixed),
|
|
663
|
-
totalFilesProcessed: files.length
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
//#endregion
|
|
591
|
+
Object.defineProperty(exports, 'CONCURRENT_FILES', {
|
|
592
|
+
enumerable: true,
|
|
593
|
+
get: function () {
|
|
594
|
+
return CONCURRENT_FILES;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
668
597
|
Object.defineProperty(exports, 'DEFAULT_FILE_PATTERN', {
|
|
669
598
|
enumerable: true,
|
|
670
599
|
get: function () {
|
|
671
600
|
return DEFAULT_FILE_PATTERN;
|
|
672
601
|
}
|
|
673
602
|
});
|
|
603
|
+
Object.defineProperty(exports, 'DEFAULT_IGNORE_PATTERNS', {
|
|
604
|
+
enumerable: true,
|
|
605
|
+
get: function () {
|
|
606
|
+
return DEFAULT_IGNORE_PATTERNS;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
674
609
|
Object.defineProperty(exports, 'DEFAULT_VERSION', {
|
|
675
610
|
enumerable: true,
|
|
676
611
|
get: function () {
|
|
@@ -701,9 +636,63 @@ Object.defineProperty(exports, '__toESM', {
|
|
|
701
636
|
return __toESM;
|
|
702
637
|
}
|
|
703
638
|
});
|
|
704
|
-
Object.defineProperty(exports, '
|
|
639
|
+
Object.defineProperty(exports, 'applyCodeActions', {
|
|
640
|
+
enumerable: true,
|
|
641
|
+
get: function () {
|
|
642
|
+
return applyCodeActions;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
Object.defineProperty(exports, 'createState', {
|
|
646
|
+
enumerable: true,
|
|
647
|
+
get: function () {
|
|
648
|
+
return createState;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
Object.defineProperty(exports, 'fileExists', {
|
|
652
|
+
enumerable: true,
|
|
653
|
+
get: function () {
|
|
654
|
+
return fileExists;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
Object.defineProperty(exports, 'findTailwindConfigPath', {
|
|
658
|
+
enumerable: true,
|
|
659
|
+
get: function () {
|
|
660
|
+
return findTailwindConfigPath;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
Object.defineProperty(exports, 'getLanguageId', {
|
|
664
|
+
enumerable: true,
|
|
665
|
+
get: function () {
|
|
666
|
+
return getLanguageId;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
Object.defineProperty(exports, 'isCssConfigFile', {
|
|
670
|
+
enumerable: true,
|
|
671
|
+
get: function () {
|
|
672
|
+
return isCssConfigFile;
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
Object.defineProperty(exports, 'loadTailwindConfig', {
|
|
676
|
+
enumerable: true,
|
|
677
|
+
get: function () {
|
|
678
|
+
return loadTailwindConfig;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
Object.defineProperty(exports, 'readFileSync', {
|
|
682
|
+
enumerable: true,
|
|
683
|
+
get: function () {
|
|
684
|
+
return readFileSync;
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
Object.defineProperty(exports, 'readGitignorePatterns', {
|
|
688
|
+
enumerable: true,
|
|
689
|
+
get: function () {
|
|
690
|
+
return readGitignorePatterns;
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
Object.defineProperty(exports, 'writeFileSync', {
|
|
705
694
|
enumerable: true,
|
|
706
695
|
get: function () {
|
|
707
|
-
return
|
|
696
|
+
return writeFileSync;
|
|
708
697
|
}
|
|
709
698
|
});
|
package/package.json
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tailwind-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A command-line tool that uses the Tailwind CSS IntelliSense plugin to show linting suggestions for your Tailwind CSS classes",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"tailwindcss",
|
|
7
|
-
"tailwind",
|
|
8
|
-
"linter",
|
|
9
|
-
"lint",
|
|
10
6
|
"cli",
|
|
7
|
+
"code-quality",
|
|
8
|
+
"css",
|
|
11
9
|
"diagnostics",
|
|
12
10
|
"intellisense",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
11
|
+
"lint",
|
|
12
|
+
"linter",
|
|
13
|
+
"tailwind",
|
|
14
|
+
"tailwindcss",
|
|
15
|
+
"utility-first"
|
|
16
16
|
],
|
|
17
17
|
"homepage": "https://github.com/ph1p/tailwind-lint#readme",
|
|
18
18
|
"bugs": {
|
|
19
19
|
"url": "https://github.com/ph1p/tailwind-lint/issues"
|
|
20
20
|
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Philip Stapelfeldt <me@ph1p.dev>",
|
|
21
23
|
"repository": {
|
|
22
24
|
"type": "git",
|
|
23
25
|
"url": "https://github.com/ph1p/tailwind-lint.git"
|
|
@@ -26,29 +28,33 @@
|
|
|
26
28
|
"type": "github",
|
|
27
29
|
"url": "https://github.com/sponsors/ph1p"
|
|
28
30
|
},
|
|
29
|
-
"license": "MIT",
|
|
30
|
-
"author": "Philip Stapelfeldt <me@ph1p.dev>",
|
|
31
|
-
"type": "module",
|
|
32
31
|
"bin": {
|
|
33
32
|
"tailwind-lint": "./dist/cli.cjs"
|
|
34
33
|
},
|
|
35
|
-
"types": "./dist/linter.d.ts",
|
|
36
34
|
"files": [
|
|
37
35
|
"dist",
|
|
38
36
|
"README.md",
|
|
39
37
|
"LICENSE"
|
|
40
38
|
],
|
|
39
|
+
"type": "module",
|
|
40
|
+
"types": "./dist/linter.d.ts",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
41
45
|
"dependencies": {
|
|
42
46
|
"@tailwindcss/language-service": "^0.14.29",
|
|
43
47
|
"chalk": "^5.6.2",
|
|
44
|
-
"commander": "^14.0.
|
|
48
|
+
"commander": "^14.0.3",
|
|
45
49
|
"fast-glob": "^3.3.3",
|
|
50
|
+
"postcss": "^8.5.6",
|
|
46
51
|
"vscode-languageserver-textdocument": "^1.0.12"
|
|
47
52
|
},
|
|
48
53
|
"devDependencies": {
|
|
49
|
-
"@
|
|
50
|
-
"
|
|
51
|
-
"
|
|
54
|
+
"@types/node": "^25.2.2",
|
|
55
|
+
"oxfmt": "^0.28.0",
|
|
56
|
+
"oxlint": "^1.43.0",
|
|
57
|
+
"tsdown": "^0.20.3",
|
|
52
58
|
"typescript": "^5.9.3",
|
|
53
59
|
"vitest": "^4.0.18"
|
|
54
60
|
},
|
|
@@ -56,15 +62,13 @@
|
|
|
56
62
|
"node": ">=22.0.0",
|
|
57
63
|
"pnpm": ">=10.0.0"
|
|
58
64
|
},
|
|
59
|
-
"publishConfig": {
|
|
60
|
-
"access": "public",
|
|
61
|
-
"registry": "https://registry.npmjs.org/"
|
|
62
|
-
},
|
|
63
65
|
"scripts": {
|
|
64
66
|
"build": "tsdown",
|
|
65
67
|
"dev": "tsdown --watch",
|
|
66
|
-
"format": "
|
|
67
|
-
"
|
|
68
|
+
"format": "oxfmt --write .",
|
|
69
|
+
"format:check": "oxfmt --check .",
|
|
70
|
+
"lint": "oxlint .",
|
|
71
|
+
"lint:fix": "oxlint --fix .",
|
|
68
72
|
"start": "node dist/cli.cjs",
|
|
69
73
|
"test": "vitest run",
|
|
70
74
|
"test:coverage": "vitest run --coverage",
|