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 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 files = [];
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: ["**/node_modules/**"],
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
- files.push(...matches);
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
- const content = readFileSync(filePath, "utf-8");
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
- diagWaiters.set(uri, res);
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.\n");
199
- console.error(" Install it: npm install -D @tailwindcss/language-server\n");
200
- process.exit(2);
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
- server.stdin.write(encode({ jsonrpc: "2.0", id, method, params }));
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
- server.stdin.write(encode({ jsonrpc: "2.0", method, params }));
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
- server.stdin.end();
227
- server.stdout.destroy();
228
- server.stderr.destroy();
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",
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",