tailwint 1.1.2 → 1.1.4

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/dist/index.js CHANGED
@@ -11,8 +11,9 @@
11
11
  import { resolve, relative } from "path";
12
12
  import { readFileSync } from "fs";
13
13
  import { glob } from "glob";
14
- import { startServer, send, notify, shutdown, fileUri, langId, diagnosticsReceived, waitForProjectReady, waitForDiagnosticCount, resetState, } from "./lsp.js";
14
+ import { startServer, send, notify, shutdown, fileUri, langId, diagnosticsReceived, settledProjects, brokenProjects, warnings, waitForAllProjects, resetState, } from "./lsp.js";
15
15
  import { fixFile } from "./edits.js";
16
+ import { prescanCssFiles } from "./prescan.js";
16
17
  import { c, setTitle, windTrail, braille, windWave, dots, tick, advanceTick, startSpinner, progressBar, banner, fileBadge, diagLine, rainbowText, celebrationAnimation, } from "./ui.js";
17
18
  // Re-export for tests
18
19
  export { applyEdits } from "./edits.js";
@@ -85,10 +86,18 @@ export async function run(options = {}) {
85
86
  notify("initialized", {});
86
87
  stopBoot();
87
88
  console.error(` ${c.green}\u2714${c.reset} ${c.dim}language server ready${c.reset} ${windTrail(30)}`);
88
- // Open files triggers the server's project discovery
89
+ // Pre-scan CSS files to predict project count
90
+ const prescan = prescanCssFiles(files);
91
+ // Open found files — triggers the server's project discovery
89
92
  const fileContents = new Map();
90
93
  const fileVersions = new Map();
94
+ const found = files.length;
95
+ const skipped = prescan.unrelatedCssFiles.size;
96
+ let sent = 0;
91
97
  for (const filePath of files) {
98
+ // Skip CSS files with no Tailwind signals — sending them wastes server CPU
99
+ if (prescan.unrelatedCssFiles.has(filePath))
100
+ continue;
92
101
  let content;
93
102
  try {
94
103
  content = readFileSync(filePath, "utf-8");
@@ -106,28 +115,52 @@ export async function run(options = {}) {
106
115
  text: content,
107
116
  },
108
117
  });
118
+ sent++;
109
119
  }
110
- // Wait for project init + diagnostics event-driven, no polling
111
- setTitle("tailwint ~ initializing...");
112
- const stopAnalyze = startSpinner(() => {
120
+ const sentText = `sent ${sent} file${sent === 1 ? "" : "s"} to lsp`;
121
+ const sentPad = 54 - 2 - sentText.length - 1;
122
+ console.error(` ${c.green}\u2714${c.reset} ${c.dim}${sentText}${c.reset} ${windTrail(sentPad)}`);
123
+ if (skipped > 0) {
124
+ const skipText = `${skipped} file${skipped === 1 ? "" : "s"} skipped`;
125
+ const skipPad = 54 - 2 - skipText.length - 1;
126
+ console.error(` ${c.dim}\u2500${c.reset} ${c.dim}${skipText}${c.reset} ${windTrail(skipPad)}`);
127
+ for (const f of prescan.unrelatedCssFiles) {
128
+ const rel = relative(cwd, f);
129
+ console.error(` ${c.dim}${fileBadge(rel)}${c.reset}`);
130
+ }
131
+ }
132
+ // Wait for all projects to be resolved (settled or broken)
133
+ setTitle("tailwint ~ scanning...");
134
+ const stopScan = startSpinner(() => {
113
135
  const received = diagnosticsReceived.size;
114
- const label = received > 0 ? "analyzing" : "initializing";
115
- setTitle(`tailwint ~ ${label} ${received}/${files.length}`);
116
- const pct = Math.round((received / files.length) * 100);
136
+ const resolved = settledProjects + brokenProjects;
137
+ const label = received > 0 ? "scanning" : "initializing";
138
+ setTitle(`tailwint ~ ${label} ${resolved}/${prescan.maxProjects}`);
139
+ const pct = sent > 0 ? Math.round((received / sent) * 100) : 0;
117
140
  const bar = progressBar(pct, 18, true);
118
- const totalStr = String(files.length);
141
+ const totalStr = String(sent);
119
142
  const recvStr = String(received).padStart(totalStr.length);
120
- const countText = `${recvStr}/${totalStr}`;
121
- const usedCols = 2 + 1 + 1 + 20 + 1 + label.length + 3 + 1 + countText.length + 1;
122
- const waveCols = Math.max(0, 56 - usedCols);
123
- return ` ${braille()} ${bar} ${c.dim}${label}${dots()}${c.reset} ${c.bold}${recvStr}${c.reset}${c.dim}/${totalStr}${c.reset} ${windTrail(waveCols, tick)}`;
143
+ const usedCols = 2 + 1 + 1 + 20 + 1 + label.length + 3 + 1 - 2;
144
+ const waveCols = Math.max(0, 54 - usedCols);
145
+ // labelCol = 2(indent) + 1(braille) + 1(space) + 20(bar) + 1(space) = 25
146
+ // "received" starts at labelCol, numbers right-aligned before it
147
+ const countStr = `${recvStr}/${totalStr}`;
148
+ const countPad = " ".repeat(Math.max(0, 25 - countStr.length - 1));
149
+ const recvLabel = "files received";
150
+ const recvUsed = countPad.length + countStr.length + 1 + recvLabel.length + 1 - 2;
151
+ const recvWave = Math.max(0, 54 - recvUsed);
152
+ 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)}`;
124
153
  }, 80);
125
- await waitForProjectReady();
126
- await waitForDiagnosticCount(files.length);
127
- stopAnalyze();
128
- const analyzedText = `${files.length} files analyzed`;
129
- const analyzePad = 54 - 2 - analyzedText.length - 1;
130
- console.error(` ${c.green}\u2714${c.reset} ${c.dim}${analyzedText}${c.reset} ${windTrail(analyzePad)}`);
154
+ await waitForAllProjects(prescan.predictedRoots, prescan.maxProjects);
155
+ stopScan();
156
+ // Log warnings for broken projects
157
+ for (const w of warnings) {
158
+ console.error(` ${c.yellow}\u26A0${c.reset} ${c.dim}${w}${c.reset}`);
159
+ }
160
+ const scanned = diagnosticsReceived.size;
161
+ const scannedText = `${scanned}/${sent} files received`;
162
+ const scannedPad = 54 - 2 - scannedText.length - 1;
163
+ console.error(` ${c.green}\u2714${c.reset} ${c.dim}${scannedText}${c.reset} ${windTrail(scannedPad)}`);
131
164
  console.error("");
132
165
  // Collect issues
133
166
  let totalIssues = 0;
@@ -149,13 +182,13 @@ export async function run(options = {}) {
149
182
  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
150
183
  setTitle("tailwint \u2714 all clear");
151
184
  await celebrationAnimation();
152
- console.error(` ${c.green}\u2714${c.reset} ${c.bold}${files.length}${c.reset} files scanned ${c.dim}// ${rainbowText("all clear")} ${c.dim}${elapsed}s${c.reset}`);
185
+ 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}`);
153
186
  console.error("");
154
187
  await shutdown();
155
188
  return 0;
156
189
  }
157
190
  // Summary
158
- console.error(` ${c.bold}${c.white}${files.length}${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}`);
191
+ 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}`);
159
192
  console.error("");
160
193
  // Report
161
194
  let fileNum = 0;
package/dist/lsp.d.ts CHANGED
@@ -3,12 +3,22 @@
3
3
  */
4
4
  export declare const diagnosticsReceived: Map<string, any[]>;
5
5
  export declare let projectReady: boolean;
6
+ /** Tracking for projectInitialized events */
7
+ export declare let projectInitCount: number;
8
+ export declare let settledProjects: number;
9
+ export declare let brokenProjects: number;
10
+ export declare const warnings: string[];
6
11
  /** Reset module state between runs (for programmatic multi-run usage). */
7
12
  export declare function resetState(): void;
8
- /** Returns a promise that resolves when @/tailwindCSS/projectInitialized fires. */
9
- export declare function waitForProjectReady(timeoutMs?: number): Promise<void>;
10
- /** Returns a promise that resolves when diagnosticsReceived.size >= count. */
11
- export declare function waitForDiagnosticCount(count: number, timeoutMs?: number): Promise<void>;
13
+ /**
14
+ * Wait for all expected projects to be resolved (settled or broken).
15
+ *
16
+ * @param predictedRoots - Number of CSS files predicted to be project roots
17
+ * @param maxProjects - Upper bound (predictedRoots + predictedNonRoots)
18
+ * @param initTimeoutMs - How long to wait for each projectInitialized event
19
+ * @param debounceMs - Silence window to consider diagnostics "settled"
20
+ */
21
+ export declare function waitForAllProjects(predictedRoots: number, maxProjects: number, initTimeoutMs?: number, debounceMs?: number): Promise<void>;
12
22
  /** Returns a promise that resolves when diagnostics are published for a specific URI. */
13
23
  export declare function waitForDiagnostic(uri: string, timeoutMs?: number): Promise<any[]>;
14
24
  export declare function startServer(root: string): void;
package/dist/lsp.js CHANGED
@@ -61,12 +61,25 @@ const pending = new Map();
61
61
  export const diagnosticsReceived = new Map();
62
62
  export let projectReady = false;
63
63
  // ---------------------------------------------------------------------------
64
- // Event-driven waiters — resolved by processMessages, no polling
64
+ // Project-aware wait state
65
65
  // ---------------------------------------------------------------------------
66
- let projectReadyResolve = null;
67
- let diagTarget = 0;
68
- let diagTargetResolve = null;
66
+ /** Tracking for projectInitialized events */
67
+ export let projectInitCount = 0;
68
+ export let settledProjects = 0;
69
+ export let brokenProjects = 0;
70
+ let lastInitMs = 0;
71
+ let inBrokenSequence = false;
72
+ let awaitingFirstDiag = false;
73
+ let currentProjectDiagCount = 0;
74
+ export const warnings = [];
75
+ /** Internal waiter state */
76
+ let projectWaitResolve = null;
77
+ let diagDebounceTimer = null;
78
+ let projectInitTimer = null;
79
+ let outerTimer = null;
69
80
  const diagWaiters = new Map();
81
+ /** Config for the current wait */
82
+ let waitConfig = { predictedRoots: 0, maxProjects: 0, initTimeoutMs: 5000, debounceMs: 500 };
70
83
  /** Reset module state between runs (for programmatic multi-run usage). */
71
84
  export function resetState() {
72
85
  msgId = 0;
@@ -76,38 +89,172 @@ export function resetState() {
76
89
  pending.clear();
77
90
  diagnosticsReceived.clear();
78
91
  projectReady = false;
79
- projectReadyResolve = null;
80
- diagTarget = 0;
81
- diagTargetResolve = null;
92
+ projectInitCount = 0;
93
+ settledProjects = 0;
94
+ brokenProjects = 0;
95
+ lastInitMs = 0;
96
+ inBrokenSequence = false;
97
+ awaitingFirstDiag = false;
98
+ currentProjectDiagCount = 0;
99
+ warnings.length = 0;
100
+ projectWaitResolve = null;
101
+ if (diagDebounceTimer) {
102
+ clearTimeout(diagDebounceTimer);
103
+ diagDebounceTimer = null;
104
+ }
105
+ if (projectInitTimer) {
106
+ clearTimeout(projectInitTimer);
107
+ projectInitTimer = null;
108
+ }
109
+ if (outerTimer) {
110
+ clearTimeout(outerTimer);
111
+ outerTimer = null;
112
+ }
82
113
  diagWaiters.clear();
83
114
  vscodeSettings = null;
84
115
  }
85
- /** Returns a promise that resolves when @/tailwindCSS/projectInitialized fires. */
86
- export function waitForProjectReady(timeoutMs = 15_000) {
87
- if (projectReady || serverDead)
88
- return Promise.resolve();
89
- return new Promise((res, rej) => {
90
- projectReadyResolve = res;
91
- const timer = setTimeout(() => {
92
- projectReadyResolve = null;
93
- res(); // resolve anyway — don't block forever
94
- }, timeoutMs);
95
- // Clean up timer if resolved early
96
- const origRes = res;
97
- projectReadyResolve = () => { clearTimeout(timer); origRes(); };
98
- });
116
+ function cleanupWaitTimers() {
117
+ if (diagDebounceTimer) {
118
+ clearTimeout(diagDebounceTimer);
119
+ diagDebounceTimer = null;
120
+ }
121
+ if (projectInitTimer) {
122
+ clearTimeout(projectInitTimer);
123
+ projectInitTimer = null;
124
+ }
125
+ if (outerTimer) {
126
+ clearTimeout(outerTimer);
127
+ outerTimer = null;
128
+ }
129
+ }
130
+ function finishWait() {
131
+ if (!projectWaitResolve)
132
+ return;
133
+ const resolve = projectWaitResolve;
134
+ projectWaitResolve = null;
135
+ cleanupWaitTimers();
136
+ resolve();
137
+ }
138
+ function isAllResolved() {
139
+ const resolved = settledProjects + brokenProjects;
140
+ return resolved >= waitConfig.maxProjects;
141
+ }
142
+ function startProjectInitTimeout() {
143
+ if (projectInitTimer)
144
+ clearTimeout(projectInitTimer);
145
+ projectInitTimer = setTimeout(() => {
146
+ // Timer fired — either no project init came, or we were waiting for
147
+ // more diagnostics after a single early one. If we got any diagnostics
148
+ // for the current project, settle it before finishing.
149
+ if (currentProjectDiagCount > 0 && !awaitingFirstDiag) {
150
+ settleCurrentProject();
151
+ }
152
+ else {
153
+ finishWait();
154
+ }
155
+ }, waitConfig.initTimeoutMs);
156
+ }
157
+ function onProjectInitialized() {
158
+ projectInitCount++;
159
+ const now = Date.now();
160
+ projectReady = true;
161
+ if (lastInitMs > 0 && (now - lastInitMs) < 500) {
162
+ // Rapid re-init — broken project
163
+ if (!inBrokenSequence) {
164
+ // First rapid init after a healthy one — the previous healthy init was actually broken
165
+ inBrokenSequence = true;
166
+ brokenProjects++;
167
+ warnings.push("A CSS file failed to initialize (likely an @apply referencing an unknown utility). " +
168
+ "That project's files will not receive diagnostics. " +
169
+ "See https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1121");
170
+ // The previous init was counted as starting a healthy project's diagnostic wait.
171
+ // Cancel that wait — this project won't produce diagnostics.
172
+ if (diagDebounceTimer) {
173
+ clearTimeout(diagDebounceTimer);
174
+ diagDebounceTimer = null;
175
+ }
176
+ }
177
+ // Additional rapid inits for the same broken project — just update timestamp
178
+ }
179
+ else {
180
+ // Healthy init — new project starting
181
+ inBrokenSequence = false;
182
+ awaitingFirstDiag = true;
183
+ currentProjectDiagCount = 0;
184
+ // Cancel any pending project-init timeout since we just got a new one
185
+ if (projectInitTimer) {
186
+ clearTimeout(projectInitTimer);
187
+ projectInitTimer = null;
188
+ }
189
+ if (diagDebounceTimer) {
190
+ clearTimeout(diagDebounceTimer);
191
+ diagDebounceTimer = null;
192
+ }
193
+ // Don't start the diagnostic debounce yet — wait for the first diagnostic to arrive.
194
+ // Use the init timeout as the safety net (if no diagnostics arrive at all,
195
+ // this project is effectively broken even though it didn't rapid-fire).
196
+ startProjectInitTimeout();
197
+ }
198
+ lastInitMs = now;
199
+ // Check if broken projects pushed us to completion
200
+ if (isAllResolved()) {
201
+ finishWait();
202
+ }
203
+ }
204
+ function settleCurrentProject() {
205
+ settledProjects++;
206
+ if (isAllResolved()) {
207
+ finishWait();
208
+ }
209
+ else {
210
+ startProjectInitTimeout();
211
+ }
212
+ }
213
+ function startDiagDebounce() {
214
+ if (diagDebounceTimer)
215
+ clearTimeout(diagDebounceTimer);
216
+ // Cancel the init timeout — we're now in diagnostic-settling mode
217
+ if (projectInitTimer) {
218
+ clearTimeout(projectInitTimer);
219
+ projectInitTimer = null;
220
+ }
221
+ diagDebounceTimer = setTimeout(settleCurrentProject, waitConfig.debounceMs);
222
+ }
223
+ function onDiagnosticReceived() {
224
+ if (!projectWaitResolve)
225
+ return;
226
+ currentProjectDiagCount++;
227
+ if (awaitingFirstDiag) {
228
+ // First diagnostic after a healthy init — don't start the debounce yet.
229
+ // The first diagnostic is often just the CSS entry point, followed by a
230
+ // ~1s pause before the bulk TSX diagnostics arrive. Starting the debounce
231
+ // here would settle too early on large projects.
232
+ awaitingFirstDiag = false;
233
+ }
234
+ else if (currentProjectDiagCount >= 2) {
235
+ // Second diagnostic and beyond — the bulk is flowing, debounce is safe
236
+ startDiagDebounce();
237
+ }
99
238
  }
100
- /** Returns a promise that resolves when diagnosticsReceived.size >= count. */
101
- export function waitForDiagnosticCount(count, timeoutMs = 30_000) {
102
- if (diagnosticsReceived.size >= count || serverDead)
239
+ /**
240
+ * Wait for all expected projects to be resolved (settled or broken).
241
+ *
242
+ * @param predictedRoots - Number of CSS files predicted to be project roots
243
+ * @param maxProjects - Upper bound (predictedRoots + predictedNonRoots)
244
+ * @param initTimeoutMs - How long to wait for each projectInitialized event
245
+ * @param debounceMs - Silence window to consider diagnostics "settled"
246
+ */
247
+ export function waitForAllProjects(predictedRoots, maxProjects, initTimeoutMs = 5_000, debounceMs = 500) {
248
+ if (serverDead || maxProjects === 0)
103
249
  return Promise.resolve();
250
+ waitConfig = { predictedRoots, maxProjects, initTimeoutMs, debounceMs };
104
251
  return new Promise((res) => {
105
- diagTarget = count;
106
- const timer = setTimeout(() => {
107
- diagTargetResolve = null;
108
- res();
109
- }, timeoutMs);
110
- diagTargetResolve = () => { clearTimeout(timer); res(); };
252
+ projectWaitResolve = res;
253
+ // Start waiting for first project init
254
+ startProjectInitTimeout();
255
+ // Hard outer timeout — never wait longer than this
256
+ const outerMs = initTimeoutMs + (maxProjects * 3000) + 5000;
257
+ outerTimer = setTimeout(finishWait, Math.min(outerMs, 30_000));
111
258
  });
112
259
  }
113
260
  /** Returns a promise that resolves when diagnostics are published for a specific URI. */
@@ -217,21 +364,12 @@ function processMessages() {
217
364
  diagWaiters.delete(uri);
218
365
  resolve(diags);
219
366
  }
220
- // Resolve count-based waiter
221
- if (diagTargetResolve && diagnosticsReceived.size >= diagTarget) {
222
- const resolve = diagTargetResolve;
223
- diagTargetResolve = null;
224
- resolve();
225
- }
367
+ // Notify the project-aware wait system
368
+ onDiagnosticReceived();
226
369
  }
227
370
  // Tailwind project initialized
228
371
  if (msg.method === "@/tailwindCSS/projectInitialized") {
229
- projectReady = true;
230
- if (projectReadyResolve) {
231
- const resolve = projectReadyResolve;
232
- projectReadyResolve = null;
233
- resolve();
234
- }
372
+ onProjectInitialized();
235
373
  }
236
374
  }
237
375
  }
@@ -249,18 +387,8 @@ function drainAll(reason) {
249
387
  p.reject(reason);
250
388
  pending.delete(id);
251
389
  }
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
- }
390
+ // Resolve project wait (so run() doesn't hang)
391
+ finishWait();
264
392
  // Resolve all URI-specific waiters with empty arrays
265
393
  for (const [uri, r] of diagWaiters) {
266
394
  r([]);
@@ -323,7 +451,10 @@ export function notify(method, params) {
323
451
  export async function shutdown() {
324
452
  if (serverDead)
325
453
  return;
326
- await send("shutdown", {}).catch(() => { });
454
+ await Promise.race([
455
+ send("shutdown", {}).catch(() => { }),
456
+ new Promise(r => setTimeout(r, 500)),
457
+ ]);
327
458
  notify("exit", {});
328
459
  serverDead = true;
329
460
  try {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pre-scan CSS files to predict how many projects the language server will create.
3
+ *
4
+ * analyzeStylesheet() is adapted from tailwindlabs/tailwindcss-intellisense
5
+ * (packages/tailwindcss-language-server/src/version-guesser.ts, MIT licensed).
6
+ */
7
+ type TailwindVersion = "3" | "4";
8
+ export interface TailwindStylesheet {
9
+ root: boolean;
10
+ versions: TailwindVersion[];
11
+ explicitImport: boolean;
12
+ }
13
+ export declare function analyzeStylesheet(content: string): TailwindStylesheet;
14
+ export interface PrescanResult {
15
+ /** Total CSS files found */
16
+ totalCssFiles: number;
17
+ /** CSS files predicted to be project roots */
18
+ predictedRoots: number;
19
+ /** CSS files that are Tailwind-related but not roots (could be promoted) */
20
+ predictedNonRoots: number;
21
+ /** CSS files with no Tailwind signals */
22
+ predictedUnrelated: number;
23
+ /** Upper bound on projects the server might create */
24
+ maxProjects: number;
25
+ /** Paths of CSS files with no Tailwind signals — safe to skip sending */
26
+ unrelatedCssFiles: Set<string>;
27
+ }
28
+ export declare function prescanCssFiles(files: string[]): PrescanResult;
29
+ export {};
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Pre-scan CSS files to predict how many projects the language server will create.
3
+ *
4
+ * analyzeStylesheet() is adapted from tailwindlabs/tailwindcss-intellisense
5
+ * (packages/tailwindcss-language-server/src/version-guesser.ts, MIT licensed).
6
+ */
7
+ import { readFileSync } from "fs";
8
+ const HAS_V4_IMPORT = /@import\s*['"]tailwindcss(?:\/[^'"]+)?['"]/;
9
+ const HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant|reference)\s*[^;{]+[;{]/;
10
+ const HAS_V4_FN = /--(alpha|spacing|theme)\(/;
11
+ const HAS_LEGACY_TAILWIND = /@tailwind\s*(base|preflight|components|variants|screens)+;/;
12
+ const HAS_TAILWIND_UTILITIES = /@tailwind\s*utilities\s*[^;]*;/;
13
+ const HAS_TAILWIND = /@tailwind\s*[^;]+;/;
14
+ const HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/;
15
+ const HAS_NON_URL_IMPORT = /@import\s*['"](?!([a-z]+:|\/\/))/;
16
+ export function analyzeStylesheet(content) {
17
+ if (HAS_V4_IMPORT.test(content)) {
18
+ return { root: true, versions: ["4"], explicitImport: true };
19
+ }
20
+ if (HAS_V4_DIRECTIVE.test(content)) {
21
+ if (HAS_TAILWIND_UTILITIES.test(content)) {
22
+ return { root: true, versions: ["4"], explicitImport: false };
23
+ }
24
+ return { root: false, versions: ["4"], explicitImport: false };
25
+ }
26
+ if (HAS_V4_FN.test(content)) {
27
+ return { root: false, versions: ["4"], explicitImport: false };
28
+ }
29
+ if (HAS_LEGACY_TAILWIND.test(content)) {
30
+ return { root: false, versions: ["3"], explicitImport: false };
31
+ }
32
+ if (HAS_TAILWIND.test(content)) {
33
+ return { root: true, versions: ["4", "3"], explicitImport: false };
34
+ }
35
+ if (HAS_COMMON_DIRECTIVE.test(content)) {
36
+ return { root: false, versions: ["4", "3"], explicitImport: false };
37
+ }
38
+ if (HAS_NON_URL_IMPORT.test(content)) {
39
+ return { root: true, versions: ["4", "3"], explicitImport: false };
40
+ }
41
+ return { root: false, versions: [], explicitImport: false };
42
+ }
43
+ export function prescanCssFiles(files) {
44
+ let predictedRoots = 0;
45
+ let predictedNonRoots = 0;
46
+ let predictedUnrelated = 0;
47
+ const unrelatedCssFiles = new Set();
48
+ for (const filePath of files) {
49
+ if (!filePath.endsWith(".css"))
50
+ continue;
51
+ let content;
52
+ try {
53
+ content = readFileSync(filePath, "utf-8");
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ const result = analyzeStylesheet(content);
59
+ if (result.versions.length === 0) {
60
+ predictedUnrelated++;
61
+ unrelatedCssFiles.add(filePath);
62
+ }
63
+ else if (result.root) {
64
+ predictedRoots++;
65
+ }
66
+ else {
67
+ predictedNonRoots++;
68
+ }
69
+ }
70
+ const totalCssFiles = predictedRoots + predictedNonRoots + predictedUnrelated;
71
+ return {
72
+ totalCssFiles,
73
+ predictedRoots,
74
+ predictedNonRoots,
75
+ unrelatedCssFiles,
76
+ predictedUnrelated,
77
+ maxProjects: predictedRoots + predictedNonRoots,
78
+ };
79
+ }
package/dist/ui.js CHANGED
@@ -88,13 +88,25 @@ export function startSpinner(render, intervalMs = 100) {
88
88
  if (!isTTY)
89
89
  return () => { };
90
90
  process.stderr.write(c.hide);
91
+ let lastLines = 1;
91
92
  const id = setInterval(() => {
92
93
  tick++;
93
- process.stderr.write(`${c.clear}${render()}`);
94
+ const output = render();
95
+ const lines = output.split("\n").length;
96
+ // Move up and clear previous lines
97
+ let clear = "\x1b[2K\r";
98
+ for (let i = 1; i < lastLines; i++)
99
+ clear = `\x1b[A\x1b[2K` + clear;
100
+ process.stderr.write(`${clear}${output}`);
101
+ lastLines = lines;
94
102
  }, intervalMs);
95
103
  return () => {
96
104
  clearInterval(id);
97
- process.stderr.write(`${c.clear}${c.show}`);
105
+ // Move cursor to the first line of the spinner
106
+ for (let i = 1; i < lastLines; i++)
107
+ process.stderr.write("\x1b[A");
108
+ // Clear from cursor to end of screen (clears all spinner lines)
109
+ process.stderr.write(`\x1b[2K\x1b[J\r${c.show}`);
98
110
  };
99
111
  }
100
112
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailwint",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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",