tailwint 1.1.6 → 1.1.8

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
@@ -50,6 +50,7 @@ npx tailwint "src/**/*.tsx"
50
50
 
51
51
  # Auto-fix all issues
52
52
  npx tailwint --fix
53
+ npx tailwint -f
53
54
 
54
55
  # Fix specific files
55
56
  npx tailwint --fix "app/**/*.tsx"
@@ -61,11 +62,13 @@ DEBUG=1 npx tailwint
61
62
  ## Example output
62
63
 
63
64
  ```
64
- ~≈∼〜~≈∼〜~≈ tailwint ∼〜~≈∼〜~≈∼~
65
+ ~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈~ tailwint ~∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼~
66
+
65
67
  tailwind css linter // powered by the official lsp
66
68
 
67
- ✔ language server ready ~≈∼〜~≈∼〜~≈∼〜
68
- ✔ 42 files analyzed ~≈∼〜~≈∼〜~≈∼〜~≈
69
+ ✔ language server ready ~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼〜~
70
+ sent 42 files to lsp ~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼〜~~
71
+ ✔ 42/42 files received ~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼〜~~
69
72
 
70
73
  42 files scanned // 8 conflicts │ 12 canonical
71
74
 
@@ -93,16 +96,16 @@ With `--fix`:
93
96
 
94
97
  ## Supported file types
95
98
 
96
- | Extension | Language ID | Notes |
97
- |-----------|-----------|-------|
98
- | `.tsx` | typescriptreact | React / Next.js components |
99
- | `.jsx` | javascriptreact | React components |
100
- | `.html` | html | Static HTML files |
101
- | `.vue` | html | Vue single-file components |
102
- | `.svelte` | html | Svelte components |
103
- | `.astro` | html | Astro components |
104
- | `.mdx` | mdx | MDX documents |
105
- | `.css` | css | `@apply` directives and Tailwind at-rules |
99
+ | Extension | Language ID | Notes |
100
+ | --------- | --------------- | ----------------------------------------- |
101
+ | `.tsx` | typescriptreact | React / Next.js components |
102
+ | `.jsx` | javascriptreact | React components |
103
+ | `.html` | html | Static HTML files |
104
+ | `.vue` | html | Vue single-file components |
105
+ | `.svelte` | html | Svelte components |
106
+ | `.astro` | html | Astro components |
107
+ | `.mdx` | mdx | MDX documents |
108
+ | `.css` | css | `@apply` directives and Tailwind at-rules |
106
109
 
107
110
  ## Tailwind v4 support
108
111
 
@@ -135,29 +138,29 @@ const exitCode = await run({
135
138
 
136
139
  ### Options
137
140
 
138
- | Option | Type | Default | Description |
139
- |--------|------|---------|-------------|
140
- | `patterns` | `string[]` | `["**/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}"]` | Glob patterns for files to scan |
141
- | `fix` | `boolean` | `false` | Auto-fix issues using LSP code actions |
142
- | `cwd` | `string` | `process.cwd()` | Working directory for glob resolution and LSP root |
141
+ | Option | Type | Default | Description |
142
+ | ---------- | ---------- | -------------------------------------------------- | -------------------------------------------------- |
143
+ | `patterns` | `string[]` | `["**/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}"]` | Glob patterns for files to scan |
144
+ | `fix` | `boolean` | `false` | Auto-fix issues using LSP code actions |
145
+ | `cwd` | `string` | `process.cwd()` | Working directory for glob resolution and LSP root |
143
146
 
144
147
  ### Exports
145
148
 
146
- | Export | Description |
147
- |--------|-------------|
148
- | `run(options?)` | Run the linter, returns exit code |
149
- | `applyEdits(content, edits)` | Apply LSP text edits to a string |
150
- | `TextEdit` | TypeScript type for LSP text edits |
149
+ | Export | Description |
150
+ | ---------------------------- | ---------------------------------- |
151
+ | `run(options?)` | Run the linter, returns exit code |
152
+ | `applyEdits(content, edits)` | Apply LSP text edits to a string |
153
+ | `TextEdit` | TypeScript type for LSP text edits |
151
154
 
152
155
  ## CI integration
153
156
 
154
157
  tailwint exits with meaningful codes for CI pipelines:
155
158
 
156
- | Exit code | Meaning |
157
- |-----------|---------|
158
- | `0` | No issues found, or all issues fixed with `--fix` |
159
- | `1` | Issues found (without `--fix`) |
160
- | `2` | Fatal error (language server not found, crash) |
159
+ | Exit code | Meaning |
160
+ | --------- | ------------------------------------------------- |
161
+ | `0` | No issues found, or all issues fixed with `--fix` |
162
+ | `1` | Issues found (without `--fix`) |
163
+ | `2` | Fatal error (language server not found, crash) |
161
164
 
162
165
  ### GitHub Actions
163
166
 
@@ -175,10 +178,11 @@ npx tailwint --fix && git add -u
175
178
  ## How it works
176
179
 
177
180
  1. **Boot** — spawns `@tailwindcss/language-server` over stdio
178
- 2. **Open** — sends all matched files to the server via `textDocument/didOpen`
179
- 3. **Analyze** — waits for `textDocument/publishDiagnostics` notifications (event-driven, no polling)
180
- 4. **Report** — collects diagnostics, categorizes as conflicts or canonical
181
- 5. **Fix** *(if `--fix`)* requests `textDocument/codeAction` quickfixes and applies edits in a loop until no diagnostics remain
181
+ 2. **Pre-scan** — classifies CSS files to predict how many Tailwind projects the server will create, skips unrelated CSS files
182
+ 3. **Open** — sends matched files to the server via `textDocument/didOpen`
183
+ 4. **Analyze** — waits for `textDocument/publishDiagnostics` notifications (event-driven, project-aware tracks each project's initialization and diagnostics separately)
184
+ 5. **Report** — collects diagnostics, categorizes as conflicts or canonical
185
+ 6. **Fix** _(if `--fix`)_ — requests `textDocument/codeAction` quickfixes and applies edits in a loop until no diagnostics remain
182
186
 
183
187
  The fix loop is unbounded — it keeps applying edits until the file stabilizes. A single pass may not resolve everything (e.g., fixing a conflict can reveal a canonical issue underneath), so the loop continues as long as edits produce changes.
184
188
 
package/dist/edits.js CHANGED
@@ -1,109 +1,2 @@
1
- /**
2
- * Text edit application and fix orchestration.
3
- */
4
- import { writeFileSync } from "fs";
5
- import { send, notify, fileUri, waitForDiagnostic } from "./lsp.js";
6
- export function applyEdits(content, edits) {
7
- if (edits.length === 0)
8
- return content;
9
- const lineOffsets = [0];
10
- for (let i = 0; i < content.length; i++) {
11
- if (content[i] === "\n")
12
- lineOffsets.push(i + 1);
13
- }
14
- function toOffset(line, char) {
15
- if (line >= lineOffsets.length)
16
- return content.length;
17
- return Math.min(lineOffsets[line] + char, content.length);
18
- }
19
- const absolute = edits.map((e) => ({
20
- start: toOffset(e.range.start.line, e.range.start.character),
21
- end: toOffset(e.range.end.line, e.range.end.character),
22
- newText: e.newText,
23
- }));
24
- absolute.sort((a, b) => a.start - b.start);
25
- const parts = [];
26
- let cursor = 0;
27
- for (const e of absolute) {
28
- if (e.start > cursor)
29
- parts.push(content.slice(cursor, e.start));
30
- parts.push(e.newText);
31
- cursor = e.end;
32
- }
33
- if (cursor < content.length)
34
- parts.push(content.slice(cursor));
35
- return parts.join("");
36
- }
37
- function rangeKey(range) {
38
- return `${range.start.line}:${range.start.character}-${range.end.line}:${range.end.character}`;
39
- }
40
- function posLte(a, b) {
41
- return a.line < b.line || (a.line === b.line && a.character <= b.character);
42
- }
43
- function rangeContains(outer, inner) {
44
- return posLte(outer.range.start, inner.range.start) && posLte(inner.range.end, outer.range.end);
45
- }
46
- function filterContainedEdits(edits) {
47
- const result = [];
48
- for (const edit of edits) {
49
- const containedByAnother = edits.some((other) => other !== edit && rangeContains(other, edit));
50
- if (!containedByAnother)
51
- result.push(edit);
52
- }
53
- return result;
54
- }
55
- async function waitForFreshDiagnostics(uri) {
56
- return waitForDiagnostic(uri);
57
- }
58
- export async function fixFile(filePath, initialDiags, fileContents, version, onPass) {
59
- const DEBUG = process.env.DEBUG === "1";
60
- const uri = fileUri(filePath);
61
- let content = fileContents.get(filePath);
62
- let ver = version.get(filePath);
63
- const issueCount = initialDiags.length;
64
- let diags = initialDiags;
65
- for (let pass = 0; diags.length > 0; pass++) {
66
- onPass?.(pass + 1);
67
- if (DEBUG)
68
- console.error(` pass ${pass + 1}: ${diags.length} remaining`);
69
- const actionResults = await Promise.all(diags.map((diag) => send("textDocument/codeAction", {
70
- textDocument: { uri },
71
- range: diag.range,
72
- context: { diagnostics: [diag], only: ["quickfix"] },
73
- }).catch(() => null)));
74
- const editsByRange = new Map();
75
- for (let i = 0; i < diags.length; i++) {
76
- const actions = actionResults[i];
77
- if (!actions || actions.length === 0)
78
- continue;
79
- const action = actions[0];
80
- const edits = action.edit?.changes?.[uri] || action.edit?.documentChanges?.[0]?.edits || [];
81
- if (edits.length === 0)
82
- continue;
83
- for (const e of edits) {
84
- const key = rangeKey(e.range);
85
- editsByRange.set(key, e);
86
- }
87
- }
88
- let candidates = [...editsByRange.values()];
89
- if (candidates.length === 0)
90
- break;
91
- candidates = filterContainedEdits(candidates);
92
- const prev = content;
93
- content = applyEdits(content, candidates);
94
- if (content === prev)
95
- break;
96
- ver++;
97
- notify("textDocument/didChange", {
98
- textDocument: { uri, version: ver },
99
- contentChanges: [{ text: content }],
100
- });
101
- diags = (await waitForFreshDiagnostics(uri)).filter((d) => d.severity === 1 || d.severity === 2);
102
- }
103
- if (content !== fileContents.get(filePath)) {
104
- writeFileSync(filePath, content);
105
- fileContents.set(filePath, content);
106
- version.set(filePath, ver);
107
- }
108
- return issueCount;
109
- }
1
+ import{writeFileSync as v}from"fs";import{send as D,notify as $,fileUri as B,waitForDiagnostic as F}from"./lsp.js";function k(t,n){if(n.length===0)return t;const r=[0];for(let e=0;e<t.length;e++)t[e]===`
2
+ `&&r.push(e+1);function c(e,o){return e>=r.length?t.length:Math.min(r[e]+o,t.length)}const a=n.map(e=>({start:c(e.range.start.line,e.range.start.character),end:c(e.range.end.line,e.range.end.character),newText:e.newText}));a.sort((e,o)=>e.start-o.start);const u=[];let i=0;for(const e of a)e.start>i&&u.push(t.slice(i,e.start)),u.push(e.newText),i=e.end;return i<t.length&&u.push(t.slice(i)),u.join("")}function C(t){return`${t.start.line}:${t.start.character}-${t.end.line}:${t.end.character}`}function b(t,n){return t.line<n.line||t.line===n.line&&t.character<=n.character}function M(t,n){return b(t.range.start,n.range.start)&&b(n.range.end,t.range.end)}function A(t){const n=[];for(const r of t)t.some(a=>a!==r&&M(a,r))||n.push(r);return n}async function U(t){return F(t)}async function R(t,n,r,c,a){const u=process.env.DEBUG==="1",i=B(t);let e=r.get(t),o=c.get(t);const y=n.length;let g=n;for(let d=0;g.length>0;d++){a?.(d+1),u&&console.error(` pass ${d+1}: ${g.length} remaining`);const E=await Promise.all(g.map(s=>D("textDocument/codeAction",{textDocument:{uri:i},range:s.range,context:{diagnostics:[s],only:["quickfix"]}}).catch(()=>null))),m=new Map;for(let s=0;s<g.length;s++){const f=E[s];if(!f||f.length===0)continue;const h=f[0],x=h.edit?.changes?.[i]||h.edit?.documentChanges?.[0]?.edits||[];if(x.length!==0)for(const p of x){const w=C(p.range);m.set(w,p)}}let l=[...m.values()];if(l.length===0)break;l=A(l);const T=e;if(e=k(e,l),e===T)break;o++,$("textDocument/didChange",{textDocument:{uri:i,version:o},contentChanges:[{text:e}]}),g=(await U(i)).filter(s=>s.severity===1||s.severity===2)}return e!==r.get(t)&&(v(t,e),r.set(t,e),c.set(t,o)),y}export{k as applyEdits,R as fixFile};
package/dist/index.js CHANGED
@@ -1,270 +1,2 @@
1
- /**
2
- * tailwint Tailwind CSS linter powered by the official language server.
3
- *
4
- * Usage: tailwint [--fix] [glob...]
5
- * tailwint # default: **\/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}
6
- * tailwint --fix # auto-fix all issues
7
- * tailwint "src/**\/*.tsx" # custom glob
8
- *
9
- * Set DEBUG=1 for verbose LSP message logging.
10
- */
11
- import { resolve, relative } from "path";
12
- import { readFileSync } from "fs";
13
- import { glob } from "glob";
14
- import { startServer, send, notify, shutdown, fileUri, langId, diagnosticsReceived, settledProjects, brokenProjects, warnings, waitForAllProjects, resetState, } from "./lsp.js";
15
- import { fixFile } from "./edits.js";
16
- import { prescanCssFiles } from "./prescan.js";
17
- import { c, setTitle, windTrail, braille, windWave, dots, tick, advanceTick, startSpinner, progressBar, banner, fileBadge, diagLine, rainbowText, celebrationAnimation, } from "./ui.js";
18
- // Re-export for tests
19
- export { applyEdits } from "./edits.js";
20
- const DEFAULT_PATTERNS = ["**/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}"];
21
- export async function run(options = {}) {
22
- resetState();
23
- const t0 = Date.now();
24
- const cwd = resolve(options.cwd || process.cwd());
25
- const fix = options.fix ?? false;
26
- const patterns = options.patterns ?? DEFAULT_PATTERNS;
27
- const fileSet = new Set();
28
- for (const pattern of patterns) {
29
- const matches = await glob(pattern, {
30
- cwd,
31
- absolute: true,
32
- nodir: true,
33
- ignore: [
34
- "**/node_modules/**",
35
- "**/dist/**",
36
- "**/build/**",
37
- "**/out/**",
38
- "**/coverage/**",
39
- "**/public/**",
40
- "**/tmp/**",
41
- "**/.tmp/**",
42
- "**/.cache/**",
43
- "**/vendor/**",
44
- "**/storybook-static/**",
45
- "**/.next/**",
46
- "**/.nuxt/**",
47
- "**/.output/**",
48
- "**/.svelte-kit/**",
49
- "**/.astro/**",
50
- "**/.vercel/**",
51
- "**/.expo/**",
52
- ],
53
- });
54
- for (const m of matches)
55
- fileSet.add(m);
56
- }
57
- const files = [...fileSet];
58
- await banner();
59
- if (files.length === 0) {
60
- const VP = 54;
61
- const l1 = "the wind blows";
62
- const l2 = "but there is nothing here";
63
- const l3 = "~ no files matched ~";
64
- const p1 = 4;
65
- const p2 = Math.floor((VP - l2.length - 2) / 2);
66
- const p3 = VP - l3.length - 8;
67
- const lw1 = p1, rw1 = VP - p1 - l1.length - 2;
68
- const lw2 = p2, rw2 = VP - p2 - l2.length - 2;
69
- const lw3 = p3, rw3 = VP - p3 - l3.length - 2;
70
- console.error(` ${windTrail(lw1)} ${c.dim}${l1}${c.reset} ${windTrail(rw1, 4)}`);
71
- console.error(` ${windTrail(lw2, 2)} ${c.dim}${l2}${c.reset} ${windTrail(rw2, 7)}`);
72
- console.error(` ${windTrail(lw3, 5)} ${c.dim}${c.italic}${l3}${c.reset} ${windTrail(rw3, 9)}`);
73
- console.error("");
74
- return 0;
75
- }
76
- // Phase 1: Boot the LSP
77
- setTitle("tailwint ~ booting...");
78
- const stopBoot = startSpinner(() => {
79
- setTitle(`tailwint ~ booting${".".repeat(Date.now() % 4)}`);
80
- return ` ${braille()} ${c.dim}booting language server${dots()}${c.reset} ${windTrail(24, tick)}`;
81
- });
82
- startServer(cwd);
83
- await send("initialize", {
84
- processId: process.pid,
85
- rootUri: fileUri(cwd),
86
- capabilities: {
87
- textDocument: {
88
- publishDiagnostics: { relatedInformation: true },
89
- codeAction: {
90
- codeActionLiteralSupport: {
91
- codeActionKind: { valueSet: ["quickfix"] },
92
- },
93
- },
94
- },
95
- workspace: { workspaceFolders: true, configuration: true },
96
- },
97
- workspaceFolders: [{ uri: fileUri(cwd), name: "workspace" }],
98
- });
99
- notify("initialized", {});
100
- stopBoot();
101
- console.error(` ${c.green}\u2714${c.reset} ${c.dim}language server ready${c.reset} ${windTrail(30)}`);
102
- // Pre-scan CSS files to predict project count
103
- const prescan = prescanCssFiles(files);
104
- // Open found files — triggers the server's project discovery
105
- const fileContents = new Map();
106
- const fileVersions = new Map();
107
- const found = files.length;
108
- const skipped = prescan.unrelatedCssFiles.size;
109
- let sent = 0;
110
- for (const filePath of files) {
111
- // Skip CSS files with no Tailwind signals — sending them wastes server CPU
112
- if (prescan.unrelatedCssFiles.has(filePath))
113
- continue;
114
- let content;
115
- try {
116
- content = readFileSync(filePath, "utf-8");
117
- }
118
- catch {
119
- continue; // file may have been deleted between glob and read
120
- }
121
- fileContents.set(filePath, content);
122
- fileVersions.set(filePath, 1);
123
- notify("textDocument/didOpen", {
124
- textDocument: {
125
- uri: fileUri(filePath),
126
- languageId: langId(filePath),
127
- version: 1,
128
- text: content,
129
- },
130
- });
131
- sent++;
132
- }
133
- const sentText = `sent ${sent} file${sent === 1 ? "" : "s"} to lsp`;
134
- const sentPad = 54 - 2 - sentText.length - 1;
135
- console.error(` ${c.green}\u2714${c.reset} ${c.dim}${sentText}${c.reset} ${windTrail(sentPad)}`);
136
- if (skipped > 0) {
137
- const skipText = `${skipped} file${skipped === 1 ? "" : "s"} skipped`;
138
- const skipPad = 54 - 2 - skipText.length - 1;
139
- console.error(` ${c.dim}\u2500${c.reset} ${c.dim}${skipText}${c.reset} ${windTrail(skipPad)}`);
140
- for (const f of prescan.unrelatedCssFiles) {
141
- const rel = relative(cwd, f);
142
- console.error(` ${c.dim}${fileBadge(rel)}${c.reset}`);
143
- }
144
- }
145
- // Wait for all projects to be resolved (settled or broken)
146
- setTitle("tailwint ~ scanning...");
147
- const stopScan = startSpinner(() => {
148
- const received = diagnosticsReceived.size;
149
- const resolved = settledProjects + brokenProjects;
150
- const label = received > 0 ? "scanning" : "initializing";
151
- setTitle(`tailwint ~ ${label} ${resolved}/${prescan.maxProjects}`);
152
- const pct = sent > 0 ? Math.round((received / sent) * 100) : 0;
153
- const bar = progressBar(pct, 18, true);
154
- const totalStr = String(sent);
155
- const recvStr = String(received).padStart(totalStr.length);
156
- const usedCols = 2 + 1 + 1 + 20 + 1 + label.length + 3 + 1 - 2;
157
- const waveCols = Math.max(0, 54 - usedCols);
158
- // labelCol = 2(indent) + 1(braille) + 1(space) + 20(bar) + 1(space) = 25
159
- // "received" starts at labelCol, numbers right-aligned before it
160
- const countStr = `${recvStr}/${totalStr}`;
161
- const countPad = " ".repeat(Math.max(0, 25 - countStr.length - 1));
162
- const recvLabel = "files received";
163
- const recvUsed = countPad.length + countStr.length + 1 + recvLabel.length + 1 - 2;
164
- const recvWave = Math.max(0, 54 - recvUsed);
165
- return ` ${braille()} ${bar} ${c.dim}${label}${dots()}${c.reset} ${windTrail(waveCols, tick)}\n${countPad}${c.bold}${countStr}${c.reset} ${c.dim}${recvLabel}${c.reset} ${windTrail(recvWave, tick + 3)}`;
166
- }, 80);
167
- await waitForAllProjects(prescan.predictedRoots, prescan.maxProjects);
168
- stopScan();
169
- // Log warnings for broken projects
170
- for (const w of warnings) {
171
- console.error(` ${c.yellow}\u26A0${c.reset} ${c.dim}${w}${c.reset}`);
172
- }
173
- const scanned = diagnosticsReceived.size;
174
- const scannedText = `${scanned}/${sent} files received`;
175
- const scannedPad = 54 - 2 - scannedText.length - 1;
176
- console.error(` ${c.green}\u2714${c.reset} ${c.dim}${scannedText}${c.reset} ${windTrail(scannedPad)}`);
177
- console.error("");
178
- // Collect issues
179
- let totalIssues = 0;
180
- const issuesByFile = new Map();
181
- for (const filePath of files) {
182
- const diags = diagnosticsReceived.get(fileUri(filePath)) || [];
183
- const meaningful = diags.filter((d) => d.severity === 1 || d.severity === 2);
184
- if (meaningful.length > 0) {
185
- issuesByFile.set(filePath, meaningful);
186
- totalIssues += meaningful.length;
187
- }
188
- }
189
- const conflicts = [...issuesByFile.values()]
190
- .flat()
191
- .filter((d) => d.code === "cssConflict").length;
192
- const canonical = totalIssues - conflicts;
193
- // All clear
194
- if (totalIssues === 0) {
195
- const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
196
- setTitle("tailwint \u2714 all clear");
197
- await celebrationAnimation();
198
- console.error(` ${c.green}\u2714${c.reset} ${c.bold}${scanned}${c.reset} files scanned ${c.dim}// ${rainbowText("all clear")} ${c.dim}${elapsed}s${c.reset}`);
199
- console.error("");
200
- await shutdown();
201
- return 0;
202
- }
203
- // Summary
204
- console.error(` ${c.bold}${c.white}${scanned}${c.reset} files scanned ${c.dim}//${c.reset} ${c.orange}${c.bold}${conflicts}${c.reset}${c.orange} conflicts${c.reset} ${c.dim}\u2502${c.reset} ${c.yellow}${c.bold}${canonical}${c.reset}${c.yellow} canonical${c.reset}`);
205
- console.error("");
206
- // Report
207
- let fileNum = 0;
208
- for (const [filePath, diags] of issuesByFile) {
209
- if (fileNum > 0)
210
- console.log(` ${c.dim}${windWave()}${c.reset}`);
211
- fileNum++;
212
- const rel = relative(cwd, filePath);
213
- console.log(` ${c.dim}\u250C${c.reset} ${fileBadge(rel)} ${c.dim}(${diags.length})${c.reset}`);
214
- for (const d of diags) {
215
- console.log(diagLine(d));
216
- }
217
- console.log(` ${c.dim}\u2514${windTrail(3)}${c.reset}`);
218
- advanceTick();
219
- }
220
- // Fix
221
- if (fix) {
222
- console.error("");
223
- console.error(` ${c.bgCyan}${c.bold} \u2699 FIX ${c.reset} ${c.dim}conflicts first, then canonical${c.reset}`);
224
- console.error("");
225
- let totalFixed = 0;
226
- let fileIdx = 0;
227
- for (const [filePath, diags] of issuesByFile) {
228
- fileIdx++;
229
- const rel = relative(cwd, filePath);
230
- let pass = 0;
231
- const shortName = rel.includes("/")
232
- ? rel.slice(rel.lastIndexOf("/") + 1)
233
- : rel;
234
- setTitle(`tailwint ~ fixing ${shortName} (${fileIdx}/${issuesByFile.size})`);
235
- const stopFix = startSpinner(() => {
236
- const pct = Math.round(((fileIdx - 1 + pass / 10) / issuesByFile.size) * 100);
237
- const bar = progressBar(pct, 18, true);
238
- const passText = `pass ${pass}`;
239
- const fixUsed = 2 + 20 + shortName.length + 1 + passText.length + 3 + 1;
240
- const fixWave = Math.max(0, 56 - fixUsed);
241
- return ` ${braille()} ${bar} ${c.bold}${c.white}${shortName}${c.reset} ${c.dim}${passText}${dots()}${c.reset} ${windTrail(fixWave, tick)}`;
242
- });
243
- const fixed = await fixFile(filePath, diags, fileContents, fileVersions, (p) => {
244
- pass = p;
245
- });
246
- stopFix();
247
- totalFixed += fixed;
248
- const pct = Math.round((fileIdx / issuesByFile.size) * 100);
249
- const bar = progressBar(pct, 18);
250
- console.error(` ${c.green}\u2714${c.reset} ${bar} ${c.bold}${c.white}${shortName}${c.reset} ${c.green}${diags.length} fixed${c.reset}`);
251
- }
252
- console.error("");
253
- const fixElapsed = ((Date.now() - t0) / 1000).toFixed(1);
254
- setTitle(`tailwint \u2714 fixed ${totalFixed} issues`);
255
- await celebrationAnimation();
256
- console.error(` ${windWave()} ${c.bgGreen}${c.bold} \u2714 FIXED ${c.reset} ${c.green}${c.bold}${totalFixed}${c.reset} of ${c.bold}${totalIssues}${c.reset} issues across ${c.bold}${issuesByFile.size}${c.reset} files ${c.dim}${fixElapsed}s${c.reset} ${windWave()}`);
257
- console.error("");
258
- await shutdown();
259
- return 0;
260
- }
261
- // Fail
262
- const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
263
- setTitle(`tailwint \u2718 ${totalIssues} issues`);
264
- console.log("");
265
- console.log(` ${windWave()} ${c.bgRed}${c.bold} \u2718 FAIL ${c.reset} ${c.red}${c.bold}${totalIssues}${c.reset} issues in ${c.bold}${issuesByFile.size}${c.reset} files ${c.dim}${elapsed}s${c.reset} ${windWave()}`);
266
- console.log(` ${c.dim}run with ${c.white}--fix${c.dim} to auto-fix${c.reset}`);
267
- console.log("");
268
- await shutdown();
269
- return 1;
270
- }
1
+ import{resolve as ne,relative as M}from"path";import{readFileSync as re}from"fs";import{glob as ie}from"glob";import{startServer as le,send as ce,notify as _,shutdown as C,fileUri as F,langId as $e,diagnosticsReceived as I,settledProjects as ae,brokenProjects as de,warnings as fe,waitForAllProjects as ge,resetState as ue}from"./lsp.js";import{fixFile as pe}from"./edits.js";import{prescanCssFiles as me}from"./prescan.js";import{c as e,setTitle as a,windTrail as r,braille as A,windWave as P,dots as V,tick as y,advanceTick as we,startSpinner as j,progressBar as E,banner as he,fileBadge as q,diagLine as xe,rainbowText as be,celebrationAnimation as G}from"./ui.js";import{applyEdits as Ve}from"./edits.js";const ve=["**/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}"];async function Ce(S={}){ue();const T=Date.now(),d=ne(S.cwd||process.cwd()),K=S.fix??!1,Y=S.patterns??ve,L=new Set;for(const t of Y){const o=await ie(t,{cwd:d,absolute:!0,nodir:!0,ignore:["**/node_modules/**","**/dist/**","**/build/**","**/out/**","**/coverage/**","**/public/**","**/tmp/**","**/.tmp/**","**/.cache/**","**/vendor/**","**/storybook-static/**","**/.next/**","**/.nuxt/**","**/.output/**","**/.svelte-kit/**","**/.astro/**","**/.vercel/**","**/.expo/**"]});for(const s of o)L.add(s)}const x=[...L];if(await he(),x.length===0){const o="the wind blows",s="but there is nothing here",n="~ no files matched ~",i=Math.floor((54-s.length-2)/2),c=54-n.length-8,$=4,v=50-o.length-2,g=i,w=54-i-s.length-2,h=c,u=54-c-n.length-2;return console.error(` ${r($)} ${e.dim}${o}${e.reset} ${r(v,4)}`),console.error(` ${r(g,2)} ${e.dim}${s}${e.reset} ${r(w,7)}`),console.error(` ${r(h,5)} ${e.dim}${e.italic}${n}${e.reset} ${r(u,9)}`),console.error(""),0}a("tailwint ~ booting...");const H=j(()=>(a(`tailwint ~ booting${".".repeat(Date.now()%4)}`),` ${A()} ${e.dim}booting language server${V()}${e.reset} ${r(24,y)}`));le(d),await ce("initialize",{processId:process.pid,rootUri:F(d),capabilities:{textDocument:{publishDiagnostics:{relatedInformation:!0},codeAction:{codeActionLiteralSupport:{codeActionKind:{valueSet:["quickfix"]}}}},workspace:{workspaceFolders:!0,configuration:!0}},workspaceFolders:[{uri:F(d),name:"workspace"}]}),_("initialized",{}),H(),console.error(` ${e.green}\u2714${e.reset} ${e.dim}language server ready${e.reset} ${r(30)}`);const p=me(x),U=new Map,B=new Map,Pe=x.length,k=p.unrelatedCssFiles.size;let f=0;for(const t of x){if(p.unrelatedCssFiles.has(t))continue;let o;try{o=re(t,"utf-8")}catch{continue}U.set(t,o),B.set(t,1),_("textDocument/didOpen",{textDocument:{uri:F(t),languageId:$e(t),version:1,text:o}}),f++}const O=`sent ${f} file${f===1?"":"s"} to lsp`,J=52-O.length-1;if(console.error(` ${e.green}\u2714${e.reset} ${e.dim}${O}${e.reset} ${r(J)}`),k>0){const t=`${k} file${k===1?"":"s"} skipped`,o=52-t.length-1;console.error(` ${e.dim}\u2500${e.reset} ${e.dim}${t}${e.reset} ${r(o)}`);for(const s of p.unrelatedCssFiles){const n=M(d,s);console.error(` ${e.dim}${q(n)}${e.reset}`)}}a("tailwint ~ scanning...");const Q=j(()=>{const t=I.size,o=ae+de,s=t>0?"scanning":"initializing";a(`tailwint ~ ${s} ${o}/${p.maxProjects}`);const n=f>0?Math.round(t/f*100):0,b=E(n,18,!0),i=String(f),c=String(t).padStart(i.length),$=25+s.length+3+1-2,v=Math.max(0,54-$),g=`${c}/${i}`,w=" ".repeat(Math.max(0,25-g.length-1)),h="files received",u=w.length+g.length+1+h.length+1-2,D=Math.max(0,54-u);return` ${A()} ${b} ${e.dim}${s}${V()}${e.reset} ${r(v,y)}
2
+ ${w}${e.bold}${g}${e.reset} ${e.dim}${h}${e.reset} ${r(D,y+3)}`},80);await ge(p.predictedRoots,p.maxProjects),Q();for(const t of fe)console.error(` ${e.yellow}\u26A0${e.reset} ${e.dim}${t}${e.reset}`);const z=I.size,R=`${z}/${f} files received`,Z=52-R.length-1;console.error(` ${e.green}\u2714${e.reset} ${e.dim}${R}${e.reset} ${r(Z)}`),console.error("");let m=0;const l=new Map;for(const t of x){const s=(I.get(F(t))||[]).filter(n=>n.severity===1||n.severity===2);s.length>0&&(l.set(t,s),m+=s.length)}const N=[...l.values()].flat().filter(t=>t.code==="cssConflict").length,ee=m-N;if(m===0){const t=((Date.now()-T)/1e3).toFixed(1);return a("tailwint \u2714 all clear"),await G(),console.error(` ${e.green}\u2714${e.reset} ${e.bold}${z}${e.reset} files scanned ${e.dim}// ${be("all clear")} ${e.dim}${t}s${e.reset}`),console.error(""),await C(),0}console.error(` ${e.bold}${e.white}${z}${e.reset} files scanned ${e.dim}//${e.reset} ${e.orange}${e.bold}${N}${e.reset}${e.orange} conflicts${e.reset} ${e.dim}\u2502${e.reset} ${e.yellow}${e.bold}${ee}${e.reset}${e.yellow} canonical${e.reset}`),console.error("");let W=0;for(const[t,o]of l){W>0&&console.log(` ${e.dim}${P()}${e.reset}`),W++;const s=M(d,t);console.log(` ${e.dim}\u250C${e.reset} ${q(s)} ${e.dim}(${o.length})${e.reset}`);for(const n of o)console.log(xe(n));console.log(` ${e.dim}\u2514${r(3)}${e.reset}`),we()}if(K){console.error(""),console.error(` ${e.bgCyan}${e.bold} \u2699 FIX ${e.reset} ${e.dim}conflicts first, then canonical${e.reset}`),console.error("");let t=0,o=0;for(const[n,b]of l){o++;const i=M(d,n);let c=0;const $=i.includes("/")?i.slice(i.lastIndexOf("/")+1):i;a(`tailwint ~ fixing ${$} (${o}/${l.size})`);const v=j(()=>{const u=Math.round((o-1+c/10)/l.size*100),D=E(u,18,!0),X=`pass ${c}`,oe=22+$.length+1+X.length+3+1,se=Math.max(0,56-oe);return` ${A()} ${D} ${e.bold}${e.white}${$}${e.reset} ${e.dim}${X}${V()}${e.reset} ${r(se,y)}`}),g=await pe(n,b,U,B,u=>{c=u});v(),t+=g;const w=Math.round(o/l.size*100),h=E(w,18);console.error(` ${e.green}\u2714${e.reset} ${h} ${e.bold}${e.white}${$}${e.reset} ${e.green}${b.length} fixed${e.reset}`)}console.error("");const s=((Date.now()-T)/1e3).toFixed(1);return a(`tailwint \u2714 fixed ${t} issues`),await G(),console.error(` ${P()} ${e.bgGreen}${e.bold} \u2714 FIXED ${e.reset} ${e.green}${e.bold}${t}${e.reset} of ${e.bold}${m}${e.reset} issues across ${e.bold}${l.size}${e.reset} files ${e.dim}${s}s${e.reset} ${P()}`),console.error(""),await C(),0}const te=((Date.now()-T)/1e3).toFixed(1);return a(`tailwint \u2718 ${m} issues`),console.log(""),console.log(` ${P()} ${e.bgRed}${e.bold} \u2718 FAIL ${e.reset} ${e.red}${e.bold}${m}${e.reset} issues in ${e.bold}${l.size}${e.reset} files ${e.dim}${te}s${e.reset} ${P()}`),console.log(` ${e.dim}run with ${e.white}--fix${e.dim} to auto-fix${e.reset}`),console.log(""),await C(),1}export{Ve as applyEdits,Ce as run};