tailwint 1.0.3 → 1.1.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 +4 -2
- package/bin/tailwint.js +40 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +33 -6
- package/dist/lsp.js +78 -13
- 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
|
*
|
|
@@ -16,23 +16,44 @@ import { fixFile } from "./edits.js";
|
|
|
16
16
|
import { c, setTitle, windTrail, braille, windWave, dots, tick, advanceTick, startSpinner, progressBar, banner, fileBadge, diagLine, rainbowText, celebrationAnimation, } from "./ui.js";
|
|
17
17
|
// Re-export for tests
|
|
18
18
|
export { applyEdits } from "./edits.js";
|
|
19
|
-
const DEFAULT_PATTERNS = ["**/*.{tsx,jsx,html,vue,svelte,css}"];
|
|
19
|
+
const DEFAULT_PATTERNS = ["**/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}"];
|
|
20
20
|
export async function run(options = {}) {
|
|
21
21
|
resetState();
|
|
22
22
|
const t0 = Date.now();
|
|
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,
|
|
30
30
|
absolute: true,
|
|
31
31
|
nodir: true,
|
|
32
|
-
ignore: [
|
|
32
|
+
ignore: [
|
|
33
|
+
"**/node_modules/**",
|
|
34
|
+
"**/dist/**",
|
|
35
|
+
"**/build/**",
|
|
36
|
+
"**/out/**",
|
|
37
|
+
"**/coverage/**",
|
|
38
|
+
"**/public/**",
|
|
39
|
+
"**/tmp/**",
|
|
40
|
+
"**/.tmp/**",
|
|
41
|
+
"**/.cache/**",
|
|
42
|
+
"**/vendor/**",
|
|
43
|
+
"**/storybook-static/**",
|
|
44
|
+
"**/.next/**",
|
|
45
|
+
"**/.nuxt/**",
|
|
46
|
+
"**/.output/**",
|
|
47
|
+
"**/.svelte-kit/**",
|
|
48
|
+
"**/.astro/**",
|
|
49
|
+
"**/.vercel/**",
|
|
50
|
+
"**/.expo/**",
|
|
51
|
+
],
|
|
33
52
|
});
|
|
34
|
-
|
|
53
|
+
for (const m of matches)
|
|
54
|
+
fileSet.add(m);
|
|
35
55
|
}
|
|
56
|
+
const files = [...fileSet];
|
|
36
57
|
await banner();
|
|
37
58
|
if (files.length === 0) {
|
|
38
59
|
console.log(` ${c.dim}No files matched.${c.reset}`);
|
|
@@ -68,7 +89,13 @@ export async function run(options = {}) {
|
|
|
68
89
|
const fileContents = new Map();
|
|
69
90
|
const fileVersions = new Map();
|
|
70
91
|
for (const filePath of files) {
|
|
71
|
-
|
|
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
|
+
}
|
|
72
99
|
fileContents.set(filePath, content);
|
|
73
100
|
fileVersions.set(filePath, 1);
|
|
74
101
|
notify("textDocument/didOpen", {
|
package/dist/lsp.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolve } from "path";
|
|
|
6
6
|
import { existsSync } from "fs";
|
|
7
7
|
const DEBUG = process.env.DEBUG === "1";
|
|
8
8
|
let server;
|
|
9
|
+
let serverDead = false;
|
|
9
10
|
let msgId = 0;
|
|
10
11
|
const chunks = [];
|
|
11
12
|
let chunksLen = 0;
|
|
@@ -22,6 +23,7 @@ const diagWaiters = new Map();
|
|
|
22
23
|
/** Reset module state between runs (for programmatic multi-run usage). */
|
|
23
24
|
export function resetState() {
|
|
24
25
|
msgId = 0;
|
|
26
|
+
serverDead = false;
|
|
25
27
|
chunks.length = 0;
|
|
26
28
|
chunksLen = 0;
|
|
27
29
|
pending.clear();
|
|
@@ -34,7 +36,7 @@ export function resetState() {
|
|
|
34
36
|
}
|
|
35
37
|
/** Returns a promise that resolves when @/tailwindCSS/projectInitialized fires. */
|
|
36
38
|
export function waitForProjectReady(timeoutMs = 15_000) {
|
|
37
|
-
if (projectReady)
|
|
39
|
+
if (projectReady || serverDead)
|
|
38
40
|
return Promise.resolve();
|
|
39
41
|
return new Promise((res, rej) => {
|
|
40
42
|
projectReadyResolve = res;
|
|
@@ -49,7 +51,7 @@ export function waitForProjectReady(timeoutMs = 15_000) {
|
|
|
49
51
|
}
|
|
50
52
|
/** Returns a promise that resolves when diagnosticsReceived.size >= count. */
|
|
51
53
|
export function waitForDiagnosticCount(count, timeoutMs = 30_000) {
|
|
52
|
-
if (diagnosticsReceived.size >= count)
|
|
54
|
+
if (diagnosticsReceived.size >= count || serverDead)
|
|
53
55
|
return Promise.resolve();
|
|
54
56
|
return new Promise((res) => {
|
|
55
57
|
diagTarget = count;
|
|
@@ -62,16 +64,18 @@ export function waitForDiagnosticCount(count, timeoutMs = 30_000) {
|
|
|
62
64
|
}
|
|
63
65
|
/** Returns a promise that resolves when diagnostics are published for a specific URI. */
|
|
64
66
|
export function waitForDiagnostic(uri, timeoutMs = 10_000) {
|
|
67
|
+
if (serverDead)
|
|
68
|
+
return Promise.resolve([]);
|
|
65
69
|
// Clear stale entry so we wait for the server to re-publish
|
|
66
70
|
diagnosticsReceived.delete(uri);
|
|
67
71
|
return new Promise((res) => {
|
|
68
|
-
|
|
69
|
-
setTimeout(() => {
|
|
72
|
+
const timer = setTimeout(() => {
|
|
70
73
|
if (diagWaiters.has(uri)) {
|
|
71
74
|
diagWaiters.delete(uri);
|
|
72
75
|
res([]);
|
|
73
76
|
}
|
|
74
77
|
}, timeoutMs);
|
|
78
|
+
diagWaiters.set(uri, (diags) => { clearTimeout(timer); res(diags); });
|
|
75
79
|
});
|
|
76
80
|
}
|
|
77
81
|
// ---------------------------------------------------------------------------
|
|
@@ -190,14 +194,46 @@ function findLanguageServer(cwd) {
|
|
|
190
194
|
const local = resolve(cwd, "node_modules/.bin/tailwindcss-language-server");
|
|
191
195
|
return existsSync(local) ? local : "tailwindcss-language-server";
|
|
192
196
|
}
|
|
197
|
+
/** Reject all pending requests and resolve all waiters. Called when the server dies. */
|
|
198
|
+
function drainAll(reason) {
|
|
199
|
+
serverDead = true;
|
|
200
|
+
for (const [id, p] of pending) {
|
|
201
|
+
p.reject(reason);
|
|
202
|
+
pending.delete(id);
|
|
203
|
+
}
|
|
204
|
+
// Resolve project-ready waiter (so run() doesn't hang)
|
|
205
|
+
if (projectReadyResolve) {
|
|
206
|
+
const r = projectReadyResolve;
|
|
207
|
+
projectReadyResolve = null;
|
|
208
|
+
r();
|
|
209
|
+
}
|
|
210
|
+
// Resolve count-based waiter
|
|
211
|
+
if (diagTargetResolve) {
|
|
212
|
+
const r = diagTargetResolve;
|
|
213
|
+
diagTargetResolve = null;
|
|
214
|
+
r();
|
|
215
|
+
}
|
|
216
|
+
// Resolve all URI-specific waiters with empty arrays
|
|
217
|
+
for (const [uri, r] of diagWaiters) {
|
|
218
|
+
r([]);
|
|
219
|
+
}
|
|
220
|
+
diagWaiters.clear();
|
|
221
|
+
}
|
|
193
222
|
export function startServer(root) {
|
|
194
223
|
const bin = findLanguageServer(root);
|
|
195
224
|
server = spawn(bin, ["--stdio"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
196
225
|
server.on("error", (err) => {
|
|
197
226
|
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
|
-
|
|
227
|
+
console.error("\n \x1b[38;5;203m\x1b[1mERROR\x1b[0m @tailwindcss/language-server not found.");
|
|
228
|
+
console.error(" Install it: \x1b[1mnpm install -D @tailwindcss/language-server\x1b[0m\n");
|
|
229
|
+
}
|
|
230
|
+
drainAll(new Error(err.code === "ENOENT"
|
|
231
|
+
? "@tailwindcss/language-server not found"
|
|
232
|
+
: `language server error: ${err.message}`));
|
|
233
|
+
});
|
|
234
|
+
server.on("close", (code, signal) => {
|
|
235
|
+
if (!serverDead) {
|
|
236
|
+
drainAll(new Error(signal ? `language server killed by ${signal}` : `language server exited with code ${code}`));
|
|
201
237
|
}
|
|
202
238
|
});
|
|
203
239
|
server.stdout.on("data", (chunk) => {
|
|
@@ -211,21 +247,48 @@ export function startServer(root) {
|
|
|
211
247
|
});
|
|
212
248
|
}
|
|
213
249
|
export function send(method, params) {
|
|
250
|
+
if (serverDead)
|
|
251
|
+
return Promise.reject(new Error("language server is not running"));
|
|
214
252
|
const id = ++msgId;
|
|
215
253
|
return new Promise((res, rej) => {
|
|
216
254
|
pending.set(id, { resolve: res, reject: rej });
|
|
217
|
-
|
|
255
|
+
try {
|
|
256
|
+
server.stdin.write(encode({ jsonrpc: "2.0", id, method, params }));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
pending.delete(id);
|
|
260
|
+
rej(new Error("language server is not running"));
|
|
261
|
+
}
|
|
218
262
|
});
|
|
219
263
|
}
|
|
220
264
|
export function notify(method, params) {
|
|
221
|
-
|
|
265
|
+
if (serverDead)
|
|
266
|
+
return;
|
|
267
|
+
try {
|
|
268
|
+
server.stdin.write(encode({ jsonrpc: "2.0", method, params }));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Server pipe is dead — drainAll will handle cleanup via the close event
|
|
272
|
+
}
|
|
222
273
|
}
|
|
223
274
|
export async function shutdown() {
|
|
275
|
+
if (serverDead)
|
|
276
|
+
return;
|
|
224
277
|
await send("shutdown", {}).catch(() => { });
|
|
225
278
|
notify("exit", {});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
279
|
+
serverDead = true;
|
|
280
|
+
try {
|
|
281
|
+
server.stdin.end();
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
284
|
+
try {
|
|
285
|
+
server.stdout.destroy();
|
|
286
|
+
}
|
|
287
|
+
catch { }
|
|
288
|
+
try {
|
|
289
|
+
server.stderr.destroy();
|
|
290
|
+
}
|
|
291
|
+
catch { }
|
|
229
292
|
server.kill();
|
|
230
293
|
}
|
|
231
294
|
export function fileUri(absPath) {
|
|
@@ -234,8 +297,10 @@ export function fileUri(absPath) {
|
|
|
234
297
|
export function langId(filePath) {
|
|
235
298
|
if (filePath.endsWith(".css"))
|
|
236
299
|
return "css";
|
|
237
|
-
if (filePath.endsWith(".html") || filePath.endsWith(".vue") || filePath.endsWith(".svelte"))
|
|
300
|
+
if (filePath.endsWith(".html") || filePath.endsWith(".vue") || filePath.endsWith(".svelte") || filePath.endsWith(".astro"))
|
|
238
301
|
return "html";
|
|
302
|
+
if (filePath.endsWith(".mdx"))
|
|
303
|
+
return "mdx";
|
|
239
304
|
if (filePath.endsWith(".jsx"))
|
|
240
305
|
return "javascriptreact";
|
|
241
306
|
return "typescriptreact";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tailwint",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|