tailwint 1.0.4 → 1.1.2
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 +4 -2
- package/bin/tailwint.js +40 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -4
- package/dist/lsp.js +126 -14
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ npm install -D tailwint @tailwindcss/language-server
|
|
|
42
42
|
## Usage
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
# Scan default file types (tsx, jsx, html, vue, svelte, css)
|
|
45
|
+
# Scan default file types (tsx, jsx, html, vue, svelte, astro, mdx, css)
|
|
46
46
|
npx tailwint
|
|
47
47
|
|
|
48
48
|
# Scan specific files
|
|
@@ -100,6 +100,8 @@ With `--fix`:
|
|
|
100
100
|
| `.html` | html | Static HTML files |
|
|
101
101
|
| `.vue` | html | Vue single-file components |
|
|
102
102
|
| `.svelte` | html | Svelte components |
|
|
103
|
+
| `.astro` | html | Astro components |
|
|
104
|
+
| `.mdx` | mdx | MDX documents |
|
|
103
105
|
| `.css` | css | `@apply` directives and Tailwind at-rules |
|
|
104
106
|
|
|
105
107
|
## Tailwind v4 support
|
|
@@ -135,7 +137,7 @@ const exitCode = await run({
|
|
|
135
137
|
|
|
136
138
|
| Option | Type | Default | Description |
|
|
137
139
|
|--------|------|---------|-------------|
|
|
138
|
-
| `patterns` | `string[]` | `["**/*.{tsx,jsx,html,vue,svelte,css}"]` | Glob patterns for files to scan |
|
|
140
|
+
| `patterns` | `string[]` | `["**/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}"]` | Glob patterns for files to scan |
|
|
139
141
|
| `fix` | `boolean` | `false` | Auto-fix issues using LSP code actions |
|
|
140
142
|
| `cwd` | `string` | `process.cwd()` | Working directory for glob resolution and LSP root |
|
|
141
143
|
|
package/bin/tailwint.js
CHANGED
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from "../dist/index.js";
|
|
3
|
+
import { shutdown } from "../dist/lsp.js";
|
|
3
4
|
import { c, isTTY } from "../dist/ui.js";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { resolve, dirname } from "path";
|
|
8
|
+
|
|
9
|
+
function cleanup(signal) {
|
|
10
|
+
if (isTTY) process.stderr.write("\x1b[?25h\x1b[2K\r");
|
|
11
|
+
shutdown().finally(() => process.exit(signal === "SIGINT" ? 130 : 143));
|
|
12
|
+
}
|
|
13
|
+
process.on("SIGINT", () => cleanup("SIGINT"));
|
|
14
|
+
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
4
15
|
|
|
5
16
|
const args = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
19
|
+
console.log(`
|
|
20
|
+
Usage: tailwint [--fix] [glob...]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--fix Auto-fix all issues using LSP code actions
|
|
24
|
+
--help Show this help message
|
|
25
|
+
--version Show version number
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
tailwint Scan default file types
|
|
29
|
+
tailwint "src/**/*.tsx" Scan specific files
|
|
30
|
+
tailwint --fix Auto-fix all issues
|
|
31
|
+
tailwint --fix "app/**/*.tsx" Fix specific files
|
|
32
|
+
|
|
33
|
+
Environment:
|
|
34
|
+
DEBUG=1 Verbose LSP message logging
|
|
35
|
+
`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
40
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
41
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
42
|
+
console.log(pkg.version);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
6
46
|
const fix = args.includes("--fix");
|
|
7
47
|
const patterns = args.filter((a) => a !== "--fix");
|
|
8
48
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* tailwint — Tailwind CSS linter powered by the official language server.
|
|
3
3
|
*
|
|
4
4
|
* Usage: tailwint [--fix] [glob...]
|
|
5
|
-
* tailwint # default: **\/*.{tsx,jsx,html,vue,svelte}
|
|
5
|
+
* tailwint # default: **\/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}
|
|
6
6
|
* tailwint --fix # auto-fix all issues
|
|
7
7
|
* tailwint "src/**\/*.tsx" # custom glob
|
|
8
8
|
*
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* tailwint — Tailwind CSS linter powered by the official language server.
|
|
3
3
|
*
|
|
4
4
|
* Usage: tailwint [--fix] [glob...]
|
|
5
|
-
* tailwint # default: **\/*.{tsx,jsx,html,vue,svelte}
|
|
5
|
+
* tailwint # default: **\/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}
|
|
6
6
|
* tailwint --fix # auto-fix all issues
|
|
7
7
|
* tailwint "src/**\/*.tsx" # custom glob
|
|
8
8
|
*
|
|
@@ -23,7 +23,7 @@ export async function run(options = {}) {
|
|
|
23
23
|
const cwd = resolve(options.cwd || process.cwd());
|
|
24
24
|
const fix = options.fix ?? false;
|
|
25
25
|
const patterns = options.patterns ?? DEFAULT_PATTERNS;
|
|
26
|
-
const
|
|
26
|
+
const fileSet = new Set();
|
|
27
27
|
for (const pattern of patterns) {
|
|
28
28
|
const matches = await glob(pattern, {
|
|
29
29
|
cwd,
|
|
@@ -43,11 +43,17 @@ export async function run(options = {}) {
|
|
|
43
43
|
"**/storybook-static/**",
|
|
44
44
|
"**/.next/**",
|
|
45
45
|
"**/.nuxt/**",
|
|
46
|
+
"**/.output/**",
|
|
46
47
|
"**/.svelte-kit/**",
|
|
48
|
+
"**/.astro/**",
|
|
49
|
+
"**/.vercel/**",
|
|
50
|
+
"**/.expo/**",
|
|
47
51
|
],
|
|
48
52
|
});
|
|
49
|
-
|
|
53
|
+
for (const m of matches)
|
|
54
|
+
fileSet.add(m);
|
|
50
55
|
}
|
|
56
|
+
const files = [...fileSet];
|
|
51
57
|
await banner();
|
|
52
58
|
if (files.length === 0) {
|
|
53
59
|
console.log(` ${c.dim}No files matched.${c.reset}`);
|
|
@@ -83,7 +89,13 @@ export async function run(options = {}) {
|
|
|
83
89
|
const fileContents = new Map();
|
|
84
90
|
const fileVersions = new Map();
|
|
85
91
|
for (const filePath of files) {
|
|
86
|
-
|
|
92
|
+
let content;
|
|
93
|
+
try {
|
|
94
|
+
content = readFileSync(filePath, "utf-8");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue; // file may have been deleted between glob and read
|
|
98
|
+
}
|
|
87
99
|
fileContents.set(filePath, content);
|
|
88
100
|
fileVersions.set(filePath, 1);
|
|
89
101
|
notify("textDocument/didOpen", {
|
package/dist/lsp.js
CHANGED
|
@@ -3,9 +3,57 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { resolve } from "path";
|
|
6
|
-
import { existsSync } from "fs";
|
|
6
|
+
import { existsSync, readFileSync } from "fs";
|
|
7
7
|
const DEBUG = process.env.DEBUG === "1";
|
|
8
|
+
let workspaceRoot = "";
|
|
9
|
+
let vscodeSettings = null;
|
|
10
|
+
/** Load .vscode/settings.json once, cache the result. */
|
|
11
|
+
function loadVscodeSettings() {
|
|
12
|
+
if (vscodeSettings !== null)
|
|
13
|
+
return vscodeSettings;
|
|
14
|
+
const settingsPath = resolve(workspaceRoot, ".vscode/settings.json");
|
|
15
|
+
if (!existsSync(settingsPath)) {
|
|
16
|
+
vscodeSettings = {};
|
|
17
|
+
return vscodeSettings;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// Strip single-line comments (// ...) and trailing commas for JSON compat
|
|
21
|
+
const raw = readFileSync(settingsPath, "utf-8")
|
|
22
|
+
.replace(/\/\/[^\n]*/g, "")
|
|
23
|
+
.replace(/,\s*([\]}])/g, "$1");
|
|
24
|
+
vscodeSettings = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
vscodeSettings = {};
|
|
28
|
+
}
|
|
29
|
+
return vscodeSettings;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract a section from flat VS Code settings into a nested object.
|
|
33
|
+
* e.g. section "tailwindCSS" turns { "tailwindCSS.lint.cssConflict": "error" }
|
|
34
|
+
* into { lint: { cssConflict: "error" } }
|
|
35
|
+
*/
|
|
36
|
+
function getSettingsSection(section) {
|
|
37
|
+
const settings = loadVscodeSettings();
|
|
38
|
+
const prefix = section + ".";
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
41
|
+
if (!key.startsWith(prefix))
|
|
42
|
+
continue;
|
|
43
|
+
const path = key.slice(prefix.length).split(".");
|
|
44
|
+
let target = result;
|
|
45
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
46
|
+
if (!(path[i] in target) || typeof target[path[i]] !== "object") {
|
|
47
|
+
target[path[i]] = {};
|
|
48
|
+
}
|
|
49
|
+
target = target[path[i]];
|
|
50
|
+
}
|
|
51
|
+
target[path[path.length - 1]] = value;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
8
55
|
let server;
|
|
56
|
+
let serverDead = false;
|
|
9
57
|
let msgId = 0;
|
|
10
58
|
const chunks = [];
|
|
11
59
|
let chunksLen = 0;
|
|
@@ -22,6 +70,7 @@ const diagWaiters = new Map();
|
|
|
22
70
|
/** Reset module state between runs (for programmatic multi-run usage). */
|
|
23
71
|
export function resetState() {
|
|
24
72
|
msgId = 0;
|
|
73
|
+
serverDead = false;
|
|
25
74
|
chunks.length = 0;
|
|
26
75
|
chunksLen = 0;
|
|
27
76
|
pending.clear();
|
|
@@ -31,10 +80,11 @@ export function resetState() {
|
|
|
31
80
|
diagTarget = 0;
|
|
32
81
|
diagTargetResolve = null;
|
|
33
82
|
diagWaiters.clear();
|
|
83
|
+
vscodeSettings = null;
|
|
34
84
|
}
|
|
35
85
|
/** Returns a promise that resolves when @/tailwindCSS/projectInitialized fires. */
|
|
36
86
|
export function waitForProjectReady(timeoutMs = 15_000) {
|
|
37
|
-
if (projectReady)
|
|
87
|
+
if (projectReady || serverDead)
|
|
38
88
|
return Promise.resolve();
|
|
39
89
|
return new Promise((res, rej) => {
|
|
40
90
|
projectReadyResolve = res;
|
|
@@ -49,7 +99,7 @@ export function waitForProjectReady(timeoutMs = 15_000) {
|
|
|
49
99
|
}
|
|
50
100
|
/** Returns a promise that resolves when diagnosticsReceived.size >= count. */
|
|
51
101
|
export function waitForDiagnosticCount(count, timeoutMs = 30_000) {
|
|
52
|
-
if (diagnosticsReceived.size >= count)
|
|
102
|
+
if (diagnosticsReceived.size >= count || serverDead)
|
|
53
103
|
return Promise.resolve();
|
|
54
104
|
return new Promise((res) => {
|
|
55
105
|
diagTarget = count;
|
|
@@ -62,16 +112,18 @@ export function waitForDiagnosticCount(count, timeoutMs = 30_000) {
|
|
|
62
112
|
}
|
|
63
113
|
/** Returns a promise that resolves when diagnostics are published for a specific URI. */
|
|
64
114
|
export function waitForDiagnostic(uri, timeoutMs = 10_000) {
|
|
115
|
+
if (serverDead)
|
|
116
|
+
return Promise.resolve([]);
|
|
65
117
|
// Clear stale entry so we wait for the server to re-publish
|
|
66
118
|
diagnosticsReceived.delete(uri);
|
|
67
119
|
return new Promise((res) => {
|
|
68
|
-
|
|
69
|
-
setTimeout(() => {
|
|
120
|
+
const timer = setTimeout(() => {
|
|
70
121
|
if (diagWaiters.has(uri)) {
|
|
71
122
|
diagWaiters.delete(uri);
|
|
72
123
|
res([]);
|
|
73
124
|
}
|
|
74
125
|
}, timeoutMs);
|
|
126
|
+
diagWaiters.set(uri, (diags) => { clearTimeout(timer); res(diags); });
|
|
75
127
|
});
|
|
76
128
|
}
|
|
77
129
|
// ---------------------------------------------------------------------------
|
|
@@ -149,7 +201,7 @@ function processMessages() {
|
|
|
149
201
|
if (msg.id != null && msg.method) {
|
|
150
202
|
let result = null;
|
|
151
203
|
if (msg.method === "workspace/configuration") {
|
|
152
|
-
result = (msg.params?.items || []).map(() => ({})
|
|
204
|
+
result = (msg.params?.items || []).map((item) => item.section ? getSettingsSection(item.section) : {});
|
|
153
205
|
}
|
|
154
206
|
server.stdin.write(encode({ jsonrpc: "2.0", id: msg.id, result }));
|
|
155
207
|
continue;
|
|
@@ -190,14 +242,47 @@ function findLanguageServer(cwd) {
|
|
|
190
242
|
const local = resolve(cwd, "node_modules/.bin/tailwindcss-language-server");
|
|
191
243
|
return existsSync(local) ? local : "tailwindcss-language-server";
|
|
192
244
|
}
|
|
245
|
+
/** Reject all pending requests and resolve all waiters. Called when the server dies. */
|
|
246
|
+
function drainAll(reason) {
|
|
247
|
+
serverDead = true;
|
|
248
|
+
for (const [id, p] of pending) {
|
|
249
|
+
p.reject(reason);
|
|
250
|
+
pending.delete(id);
|
|
251
|
+
}
|
|
252
|
+
// Resolve project-ready waiter (so run() doesn't hang)
|
|
253
|
+
if (projectReadyResolve) {
|
|
254
|
+
const r = projectReadyResolve;
|
|
255
|
+
projectReadyResolve = null;
|
|
256
|
+
r();
|
|
257
|
+
}
|
|
258
|
+
// Resolve count-based waiter
|
|
259
|
+
if (diagTargetResolve) {
|
|
260
|
+
const r = diagTargetResolve;
|
|
261
|
+
diagTargetResolve = null;
|
|
262
|
+
r();
|
|
263
|
+
}
|
|
264
|
+
// Resolve all URI-specific waiters with empty arrays
|
|
265
|
+
for (const [uri, r] of diagWaiters) {
|
|
266
|
+
r([]);
|
|
267
|
+
}
|
|
268
|
+
diagWaiters.clear();
|
|
269
|
+
}
|
|
193
270
|
export function startServer(root) {
|
|
271
|
+
workspaceRoot = root;
|
|
194
272
|
const bin = findLanguageServer(root);
|
|
195
273
|
server = spawn(bin, ["--stdio"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
196
274
|
server.on("error", (err) => {
|
|
197
275
|
if (err.code === "ENOENT") {
|
|
198
|
-
console.error("\n \x1b[38;5;203m\x1b[1mERROR\x1b[0m @tailwindcss/language-server not found
|
|
199
|
-
console.error(" Install it:
|
|
200
|
-
|
|
276
|
+
console.error("\n \x1b[38;5;203m\x1b[1mERROR\x1b[0m @tailwindcss/language-server not found.");
|
|
277
|
+
console.error(" Install it: \x1b[1mnpm install -D @tailwindcss/language-server\x1b[0m\n");
|
|
278
|
+
}
|
|
279
|
+
drainAll(new Error(err.code === "ENOENT"
|
|
280
|
+
? "@tailwindcss/language-server not found"
|
|
281
|
+
: `language server error: ${err.message}`));
|
|
282
|
+
});
|
|
283
|
+
server.on("close", (code, signal) => {
|
|
284
|
+
if (!serverDead) {
|
|
285
|
+
drainAll(new Error(signal ? `language server killed by ${signal}` : `language server exited with code ${code}`));
|
|
201
286
|
}
|
|
202
287
|
});
|
|
203
288
|
server.stdout.on("data", (chunk) => {
|
|
@@ -211,21 +296,48 @@ export function startServer(root) {
|
|
|
211
296
|
});
|
|
212
297
|
}
|
|
213
298
|
export function send(method, params) {
|
|
299
|
+
if (serverDead)
|
|
300
|
+
return Promise.reject(new Error("language server is not running"));
|
|
214
301
|
const id = ++msgId;
|
|
215
302
|
return new Promise((res, rej) => {
|
|
216
303
|
pending.set(id, { resolve: res, reject: rej });
|
|
217
|
-
|
|
304
|
+
try {
|
|
305
|
+
server.stdin.write(encode({ jsonrpc: "2.0", id, method, params }));
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
pending.delete(id);
|
|
309
|
+
rej(new Error("language server is not running"));
|
|
310
|
+
}
|
|
218
311
|
});
|
|
219
312
|
}
|
|
220
313
|
export function notify(method, params) {
|
|
221
|
-
|
|
314
|
+
if (serverDead)
|
|
315
|
+
return;
|
|
316
|
+
try {
|
|
317
|
+
server.stdin.write(encode({ jsonrpc: "2.0", method, params }));
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Server pipe is dead — drainAll will handle cleanup via the close event
|
|
321
|
+
}
|
|
222
322
|
}
|
|
223
323
|
export async function shutdown() {
|
|
324
|
+
if (serverDead)
|
|
325
|
+
return;
|
|
224
326
|
await send("shutdown", {}).catch(() => { });
|
|
225
327
|
notify("exit", {});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
328
|
+
serverDead = true;
|
|
329
|
+
try {
|
|
330
|
+
server.stdin.end();
|
|
331
|
+
}
|
|
332
|
+
catch { }
|
|
333
|
+
try {
|
|
334
|
+
server.stdout.destroy();
|
|
335
|
+
}
|
|
336
|
+
catch { }
|
|
337
|
+
try {
|
|
338
|
+
server.stderr.destroy();
|
|
339
|
+
}
|
|
340
|
+
catch { }
|
|
229
341
|
server.kill();
|
|
230
342
|
}
|
|
231
343
|
export function fileUri(absPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tailwint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Tailwind CSS linter for CI — drives the official language server to catch class issues and auto-fix them",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Peter Wang",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"diagnostics",
|
|
25
25
|
"code-quality"
|
|
26
26
|
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
27
30
|
"type": "module",
|
|
28
31
|
"main": "./dist/index.js",
|
|
29
32
|
"types": "./dist/index.d.ts",
|