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 +54 -21
- package/dist/lsp.d.ts +14 -4
- package/dist/lsp.js +186 -55
- package/dist/prescan.d.ts +29 -0
- package/dist/prescan.js +79 -0
- package/dist/ui.js +14 -2
- package/package.json +1 -1
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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(
|
|
141
|
+
const totalStr = String(sent);
|
|
119
142
|
const recvStr = String(received).padStart(totalStr.length);
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
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}${
|
|
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}${
|
|
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
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
//
|
|
64
|
+
// Project-aware wait state
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
let
|
|
68
|
-
let
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
//
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
253
|
-
|
|
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
|
|
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 {};
|
package/dist/prescan.js
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|