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 +36 -32
- package/dist/edits.js +2 -109
- package/dist/index.js +2 -270
- package/dist/lsp.js +7 -487
- package/dist/prescan.js +1 -79
- package/dist/ui.js +3 -203
- package/package.json +4 -3
- package/dist/edits.test.d.ts +0 -4
- package/dist/edits.test.js +0 -410
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
|
-
|
|
65
|
+
~≈∼〜~≈∼〜~≈∼〜~≈∼〜~≈~ tailwint ~∼〜~≈∼〜~≈∼〜~≈∼〜~≈∼~
|
|
66
|
+
|
|
65
67
|
tailwind css linter // powered by the official lsp
|
|
66
68
|
|
|
67
|
-
✔ language server ready
|
|
68
|
-
✔ 42 files
|
|
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
|
|
97
|
-
|
|
98
|
-
| `.tsx`
|
|
99
|
-
| `.jsx`
|
|
100
|
-
| `.html`
|
|
101
|
-
| `.vue`
|
|
102
|
-
| `.svelte` | html
|
|
103
|
-
| `.astro`
|
|
104
|
-
| `.mdx`
|
|
105
|
-
| `.css`
|
|
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
|
|
139
|
-
|
|
140
|
-
| `patterns` | `string[]` | `["**/*.{tsx,jsx,html,vue,svelte,astro,mdx,css}"]` | Glob patterns for files to scan
|
|
141
|
-
| `fix`
|
|
142
|
-
| `cwd`
|
|
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
|
|
147
|
-
|
|
148
|
-
| `run(options?)`
|
|
149
|
-
| `applyEdits(content, edits)` | Apply LSP text edits to a string
|
|
150
|
-
| `TextEdit`
|
|
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`
|
|
159
|
-
| `1`
|
|
160
|
-
| `2`
|
|
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. **
|
|
179
|
-
3. **
|
|
180
|
-
4. **
|
|
181
|
-
5. **
|
|
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
|
-
|
|
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
|
|
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};
|