@knip/language-server 3.1.1 → 3.1.2
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/package.json +4 -4
- package/src/server.js +25 -6
- package/test/fixtures/lifecycle.js +14 -0
- package/test/lifecycle.test.js +221 -0
- package/test/refresh.test.js +114 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knip/language-server",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/webpro-nl/knip.git",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"./constants": "./src/constants.js"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"
|
|
22
|
-
"vscode-languageserver
|
|
23
|
-
"
|
|
21
|
+
"knip": "^6.44.0",
|
|
22
|
+
"vscode-languageserver": "^10.1.2",
|
|
23
|
+
"vscode-languageserver-textdocument": "^1.0.16"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": "^20.19.0 || >=22.12.0"
|
package/src/server.js
CHANGED
|
@@ -99,6 +99,7 @@ export class LanguageServer {
|
|
|
99
99
|
/** @type {undefined | Session} */
|
|
100
100
|
session;
|
|
101
101
|
|
|
102
|
+
revision = 0;
|
|
102
103
|
/** @type {Rules} */
|
|
103
104
|
rules = {};
|
|
104
105
|
|
|
@@ -152,8 +153,9 @@ export class LanguageServer {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
this.connection.onInitialized(async () => {
|
|
156
|
+
const revision = this.revision;
|
|
155
157
|
const config = await this.getConfig();
|
|
156
|
-
if (config?.deferSession !== true) this.start();
|
|
158
|
+
if (revision === this.revision && config?.deferSession !== true) this.start();
|
|
157
159
|
});
|
|
158
160
|
|
|
159
161
|
this.connection.onRequest(REQUEST_START, () => this.start());
|
|
@@ -244,10 +246,13 @@ export class LanguageServer {
|
|
|
244
246
|
|
|
245
247
|
async start() {
|
|
246
248
|
if (this.session) return;
|
|
249
|
+
const revision = ++this.revision;
|
|
247
250
|
|
|
248
251
|
try {
|
|
249
252
|
const config = await this.getConfig();
|
|
253
|
+
if (revision !== this.revision) return;
|
|
250
254
|
const knip = await this.#resolveKnipSession();
|
|
255
|
+
if (revision !== this.revision) return;
|
|
251
256
|
|
|
252
257
|
const configFilePath = config?.configFilePath
|
|
253
258
|
? path.isAbsolute(config.configFilePath)
|
|
@@ -257,31 +262,38 @@ export class LanguageServer {
|
|
|
257
262
|
|
|
258
263
|
this.connection.console.log('Creating options');
|
|
259
264
|
const options = await knip.createOptions({ cwd: this.cwd, isSession: true, args: { config: configFilePath } });
|
|
265
|
+
if (revision !== this.revision) return;
|
|
260
266
|
this.rules = options.rules;
|
|
261
267
|
|
|
262
268
|
this.connection.console.log('Building module graph...');
|
|
263
269
|
const start = Date.now();
|
|
264
270
|
const session = await knip.createSession(options);
|
|
271
|
+
if (revision !== this.revision) return;
|
|
265
272
|
this.connection.console.log(`Finished building module graph (${Date.now() - start}ms)`);
|
|
266
273
|
|
|
267
274
|
this.session = session;
|
|
268
275
|
this.publishDiagnostics(this.buildDiagnostics(session.getIssues().issues, config, this.rules));
|
|
269
276
|
this.connection.sendNotification(NOTIFICATION_MODULE_GRAPH_BUILT, { duration: Date.now() - start });
|
|
270
277
|
} catch (_error) {
|
|
278
|
+
if (revision !== this.revision) return;
|
|
271
279
|
this.connection.console.error(`Error: ${_error}`);
|
|
272
280
|
}
|
|
273
281
|
}
|
|
274
282
|
|
|
275
283
|
stop() {
|
|
284
|
+
this.revision++;
|
|
276
285
|
this.session = undefined;
|
|
277
286
|
this.fileCache = undefined;
|
|
287
|
+
this.packageJsonCache = undefined;
|
|
288
|
+
this.cycleDiagnostics.clear();
|
|
289
|
+
this.issuesByUri.clear();
|
|
278
290
|
for (const uri of this.published) this.connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
279
291
|
this.published.clear();
|
|
280
292
|
}
|
|
281
293
|
|
|
282
|
-
restart() {
|
|
294
|
+
async restart() {
|
|
283
295
|
this.stop();
|
|
284
|
-
this.start();
|
|
296
|
+
await this.start();
|
|
285
297
|
}
|
|
286
298
|
|
|
287
299
|
getResults() {
|
|
@@ -297,6 +309,7 @@ export class LanguageServer {
|
|
|
297
309
|
this.fileCache = undefined;
|
|
298
310
|
this.packageJsonCache = undefined;
|
|
299
311
|
if (!this.session) return;
|
|
312
|
+
const session = this.session;
|
|
300
313
|
|
|
301
314
|
const cwd = this.cwd ?? process.cwd();
|
|
302
315
|
|
|
@@ -315,17 +328,23 @@ export class LanguageServer {
|
|
|
315
328
|
if (changes.length === 0) return;
|
|
316
329
|
|
|
317
330
|
try {
|
|
318
|
-
const result = await
|
|
331
|
+
const result = await session.handleFileChanges(changes);
|
|
319
332
|
|
|
320
|
-
if (!result) return;
|
|
333
|
+
if (!result || this.session !== session) return;
|
|
334
|
+
this.fileCache = undefined;
|
|
335
|
+
this.packageJsonCache = undefined;
|
|
336
|
+
this.cycleDiagnostics.clear();
|
|
321
337
|
|
|
322
338
|
this.connection.console.log(
|
|
323
339
|
`Module graph updated (${Math.floor(result.duration)}ms • ${(result.mem / 1024 / 1024).toFixed(2)}M)`
|
|
324
340
|
);
|
|
325
341
|
|
|
326
342
|
const config = await this.getConfig();
|
|
327
|
-
|
|
343
|
+
if (this.session !== session) return;
|
|
344
|
+
this.publishDiagnostics(this.buildDiagnostics(session.getIssues().issues, config, this.rules));
|
|
345
|
+
this.connection.sendNotification(NOTIFICATION_MODULE_GRAPH_BUILT, { duration: result.duration });
|
|
328
346
|
} catch (_error) {
|
|
347
|
+
if (this.session !== session) return;
|
|
329
348
|
this.connection.console.error(`Error handling file changes: ${_error}`);
|
|
330
349
|
}
|
|
331
350
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
2
|
+
import {
|
|
3
|
+
createOptions as options,
|
|
4
|
+
createSession as session,
|
|
5
|
+
KNIP_CONFIG_LOCATIONS,
|
|
6
|
+
} from 'knip/session';
|
|
7
|
+
import { CodeActionKind, createConnection as connect } from 'vscode-languageserver/node';
|
|
8
|
+
|
|
9
|
+
export { CodeActionKind, KNIP_CONFIG_LOCATIONS };
|
|
10
|
+
export const knip = { createOptions: options, createSession: session };
|
|
11
|
+
export const createOptions = options => knip.createOptions(options);
|
|
12
|
+
export const createSession = (...args) => knip.createSession(...args);
|
|
13
|
+
export const connection = connect(new PassThrough(), new PassThrough());
|
|
14
|
+
export const createConnection = () => connection;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { registerHooks } from 'node:module';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import test, { after } from 'node:test';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { NOTIFICATION_MODULE_GRAPH_BUILT, SESSION_LOADING } from '../src/constants.js';
|
|
9
|
+
import { connection, knip } from './fixtures/lifecycle.js';
|
|
10
|
+
|
|
11
|
+
const serverUrl = new URL('../src/server.js', import.meta.url).href;
|
|
12
|
+
const fixtureUrl = new URL('./fixtures/lifecycle.js', import.meta.url).href;
|
|
13
|
+
registerHooks({
|
|
14
|
+
resolve(specifier, context, nextResolve) {
|
|
15
|
+
if (
|
|
16
|
+
context.parentURL === serverUrl &&
|
|
17
|
+
(specifier === 'vscode-languageserver/node' || specifier === 'knip/session')
|
|
18
|
+
) {
|
|
19
|
+
return { url: fixtureUrl, shortCircuit: true };
|
|
20
|
+
}
|
|
21
|
+
return nextResolve(specifier, context);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const { LanguageServer } = await import('../src/server.js');
|
|
26
|
+
const cwd = mkdtempSync(path.join(tmpdir(), 'knip-ls-lifecycle-'));
|
|
27
|
+
after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
28
|
+
const filePath = path.join(cwd, 'value.js');
|
|
29
|
+
const changes = { changes: [{ uri: pathToFileURL(filePath).href, type: 2 }] };
|
|
30
|
+
const updated = { duration: 1, mem: 1024 };
|
|
31
|
+
const config = { editor: { exports: { quickfix: { enabled: true } } } };
|
|
32
|
+
const options = { rules: { exports: 'warn' } };
|
|
33
|
+
const deferred = () => Promise.withResolvers();
|
|
34
|
+
|
|
35
|
+
const createSession = (name = 'current') => ({
|
|
36
|
+
getIssues: () => ({ issues: {} }),
|
|
37
|
+
getResults: () => ({ name }),
|
|
38
|
+
describeFile: () => ({ name, metrics: {} }),
|
|
39
|
+
describePackageJson: () => ({ dependenciesUsage: new Map([[name, { imports: [] }]]) }),
|
|
40
|
+
handleFileChanges: async () => updated,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const setup = t => {
|
|
44
|
+
t.mock.method(connection, 'listen', () => {});
|
|
45
|
+
t.mock.method(connection.workspace, 'getConfiguration', async () => config);
|
|
46
|
+
t.mock.method(connection.console, 'log', () => {});
|
|
47
|
+
t.mock.method(connection.console, 'error', () => {});
|
|
48
|
+
t.mock.method(connection, 'sendDiagnostics', () => {});
|
|
49
|
+
t.mock.method(connection, 'sendNotification', () => {});
|
|
50
|
+
t.mock.method(connection, 'onInitialized');
|
|
51
|
+
t.mock.method(knip, 'createOptions', async () => options);
|
|
52
|
+
t.mock.method(knip, 'createSession', async () => createSession());
|
|
53
|
+
const originalConsole = { ...console };
|
|
54
|
+
const server = new LanguageServer();
|
|
55
|
+
Object.assign(console, originalConsole);
|
|
56
|
+
server.cwd = cwd;
|
|
57
|
+
return server;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
test('stop cancels automatic startup while initialization configuration is pending', async t => {
|
|
61
|
+
const server = setup(t);
|
|
62
|
+
const pending = deferred();
|
|
63
|
+
t.mock.method(server, 'getConfig', () => pending.promise);
|
|
64
|
+
const start = t.mock.method(server, 'start');
|
|
65
|
+
const initialized = connection.onInitialized.mock.calls[0].arguments[0]();
|
|
66
|
+
server.stop();
|
|
67
|
+
pending.resolve(config);
|
|
68
|
+
await initialized;
|
|
69
|
+
for (const call of start.mock.calls) await call.result;
|
|
70
|
+
assert.equal(server.getResults(), null);
|
|
71
|
+
assert.equal(connection.sendNotification.mock.callCount(), 0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const phase of ['createOptions', 'createSession']) {
|
|
75
|
+
test(`stop discards a pending ${phase}`, { timeout: 3000 }, async t => {
|
|
76
|
+
const server = setup(t);
|
|
77
|
+
const entered = deferred();
|
|
78
|
+
const pending = deferred();
|
|
79
|
+
t.mock.method(knip, phase, () => {
|
|
80
|
+
entered.resolve();
|
|
81
|
+
return pending.promise;
|
|
82
|
+
});
|
|
83
|
+
const start = server.start();
|
|
84
|
+
await entered.promise;
|
|
85
|
+
server.stop();
|
|
86
|
+
pending.resolve(phase === 'createOptions' ? options : createSession('obsolete'));
|
|
87
|
+
await start;
|
|
88
|
+
assert.equal(server.getResults(), null);
|
|
89
|
+
assert.equal(server.getFileDescriptor(filePath), SESSION_LOADING);
|
|
90
|
+
assert.equal(connection.sendNotification.mock.callCount(), 0);
|
|
91
|
+
assert.equal(connection.sendDiagnostics.mock.callCount(), 0);
|
|
92
|
+
if (phase === 'createOptions') assert.equal(knip.createSession.mock.callCount(), 0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test(`restart keeps the new session when obsolete ${phase} finishes last`, { timeout: 3000 }, async t => {
|
|
96
|
+
const server = setup(t);
|
|
97
|
+
const entered = deferred();
|
|
98
|
+
const pending = deferred();
|
|
99
|
+
let calls = 0;
|
|
100
|
+
t.mock.method(knip, phase, () => {
|
|
101
|
+
if (++calls > 1) return phase === 'createOptions' ? options : createSession();
|
|
102
|
+
entered.resolve();
|
|
103
|
+
return pending.promise;
|
|
104
|
+
});
|
|
105
|
+
const start = server.start();
|
|
106
|
+
await entered.promise;
|
|
107
|
+
await server.restart();
|
|
108
|
+
assert.deepEqual(server.getResults(), { name: 'current' });
|
|
109
|
+
pending.resolve(phase === 'createOptions' ? { rules: { exports: 'error' } } : createSession('obsolete'));
|
|
110
|
+
await start;
|
|
111
|
+
assert.deepEqual(server.getResults(), { name: 'current' });
|
|
112
|
+
assert.deepEqual(server.rules, options.rules);
|
|
113
|
+
assert.equal(connection.sendNotification.mock.callCount(), 1);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test('overlapping starts retain the latest session and ignore obsolete startup errors', { timeout: 3000 }, async t => {
|
|
118
|
+
const server = setup(t);
|
|
119
|
+
const entered = deferred();
|
|
120
|
+
const pending = deferred();
|
|
121
|
+
let calls = 0;
|
|
122
|
+
t.mock.method(knip, 'createSession', () => {
|
|
123
|
+
if (++calls > 1) return createSession();
|
|
124
|
+
entered.resolve();
|
|
125
|
+
return pending.promise;
|
|
126
|
+
});
|
|
127
|
+
const start = server.start();
|
|
128
|
+
await entered.promise;
|
|
129
|
+
await server.start();
|
|
130
|
+
pending.reject(new Error('obsolete startup failure'));
|
|
131
|
+
await start;
|
|
132
|
+
assert.deepEqual(server.getResults(), { name: 'current' });
|
|
133
|
+
assert.equal(connection.console.error.mock.callCount(), 0);
|
|
134
|
+
assert.equal(connection.sendNotification.mock.callCount(), 1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('graph updates invalidate descriptors requested during the update and notify without diagnostics', async t => {
|
|
138
|
+
const server = setup(t);
|
|
139
|
+
let session = createSession('before');
|
|
140
|
+
const pending = deferred();
|
|
141
|
+
server.session = {
|
|
142
|
+
...session,
|
|
143
|
+
describeFile: () => session.describeFile(),
|
|
144
|
+
describePackageJson: () => session.describePackageJson(),
|
|
145
|
+
handleFileChanges: () => pending.promise,
|
|
146
|
+
};
|
|
147
|
+
const update = server.handleFileChanges(changes);
|
|
148
|
+
assert.equal(server.getFileDescriptor(filePath).name, 'before');
|
|
149
|
+
assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['before']);
|
|
150
|
+
session = createSession('after');
|
|
151
|
+
pending.resolve(updated);
|
|
152
|
+
await update;
|
|
153
|
+
assert.equal(server.getFileDescriptor(filePath).name, 'after');
|
|
154
|
+
assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['after']);
|
|
155
|
+
assert.equal(connection.sendDiagnostics.mock.callCount(), 0);
|
|
156
|
+
assert.deepEqual(connection.sendNotification.mock.calls[0]?.arguments, [
|
|
157
|
+
NOTIFICATION_MODULE_GRAPH_BUILT,
|
|
158
|
+
{ duration: updated.duration },
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('stop clears package descriptors before another session starts', async t => {
|
|
163
|
+
const server = setup(t);
|
|
164
|
+
server.session = createSession('obsolete');
|
|
165
|
+
assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['obsolete']);
|
|
166
|
+
server.stop();
|
|
167
|
+
await server.start();
|
|
168
|
+
assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['current']);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
for (const replaced of [false, true]) {
|
|
172
|
+
for (const phase of ['update', 'configuration']) {
|
|
173
|
+
test(`discard an obsolete update awaiting ${phase} after ${replaced ? 'replacement' : 'stop'}`, async t => {
|
|
174
|
+
const server = setup(t);
|
|
175
|
+
const pending = deferred();
|
|
176
|
+
const entered = deferred();
|
|
177
|
+
const old = createSession('obsolete');
|
|
178
|
+
server.session = old;
|
|
179
|
+
if (phase === 'update') old.handleFileChanges = () => pending.promise;
|
|
180
|
+
else {
|
|
181
|
+
t.mock.method(server, 'getConfig', () => {
|
|
182
|
+
entered.resolve();
|
|
183
|
+
return pending.promise;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const update = server.handleFileChanges(changes);
|
|
187
|
+
if (phase === 'configuration') await entered.promise;
|
|
188
|
+
server.stop();
|
|
189
|
+
if (replaced) {
|
|
190
|
+
server.session = createSession();
|
|
191
|
+
server.session.getIssues = () => ({ issues: { files: { [filePath]: { file: { type: 'files', filePath } } } } });
|
|
192
|
+
server.getFileDescriptor(filePath);
|
|
193
|
+
server.getPackageJsonDescriptor();
|
|
194
|
+
}
|
|
195
|
+
const file = server.fileCache;
|
|
196
|
+
const manifest = server.packageJsonCache;
|
|
197
|
+
pending.resolve(phase === 'update' ? updated : config);
|
|
198
|
+
await update;
|
|
199
|
+
assert.equal(server.fileCache, file);
|
|
200
|
+
assert.equal(server.packageJsonCache, manifest);
|
|
201
|
+
assert.equal(connection.sendDiagnostics.mock.callCount(), 0);
|
|
202
|
+
assert.equal(connection.sendNotification.mock.callCount(), 0);
|
|
203
|
+
assert.equal(connection.console.error.mock.callCount(), 0);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
test('obsolete update errors cannot affect the replacement session', async t => {
|
|
209
|
+
const server = setup(t);
|
|
210
|
+
await server.start();
|
|
211
|
+
const pending = deferred();
|
|
212
|
+
server.session.handleFileChanges = () => pending.promise;
|
|
213
|
+
const update = server.handleFileChanges(changes);
|
|
214
|
+
await server.restart();
|
|
215
|
+
pending.reject(new Error('obsolete update failure'));
|
|
216
|
+
await update;
|
|
217
|
+
assert.deepEqual(server.getResults(), { name: 'current' });
|
|
218
|
+
assert.deepEqual(server.rules, options.rules);
|
|
219
|
+
assert.equal(connection.console.error.mock.callCount(), 0);
|
|
220
|
+
assert.equal(connection.sendNotification.mock.callCount(), 2);
|
|
221
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { cpSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
import { NOTIFICATION_MODULE_GRAPH_BUILT, REQUEST_FILE_NODE, REQUEST_RESULTS } from '../src/constants.js';
|
|
9
|
+
|
|
10
|
+
const startServer = (cwd, t) => {
|
|
11
|
+
const child = spawn(process.execPath, [fileURLToPath(new URL('../src/cli.js', import.meta.url)), '--stdio'], {
|
|
12
|
+
cwd,
|
|
13
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
14
|
+
});
|
|
15
|
+
t.after(() => child.kill());
|
|
16
|
+
let buffer = Buffer.alloc(0);
|
|
17
|
+
let nextId = 1;
|
|
18
|
+
const messages = [];
|
|
19
|
+
const send = payload => {
|
|
20
|
+
const body = JSON.stringify({ jsonrpc: '2.0', ...payload });
|
|
21
|
+
child.stdin.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
22
|
+
};
|
|
23
|
+
child.stdout.on('data', chunk => {
|
|
24
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
25
|
+
for (;;) {
|
|
26
|
+
const header = buffer.indexOf('\r\n\r\n');
|
|
27
|
+
if (header === -1) return;
|
|
28
|
+
const length = Number(
|
|
29
|
+
buffer
|
|
30
|
+
.subarray(0, header)
|
|
31
|
+
.toString()
|
|
32
|
+
.match(/Content-Length: (\d+)/i)?.[1]
|
|
33
|
+
);
|
|
34
|
+
if (buffer.length < header + 4 + length) return;
|
|
35
|
+
const message = JSON.parse(buffer.subarray(header + 4, header + 4 + length).toString());
|
|
36
|
+
buffer = buffer.subarray(header + 4 + length);
|
|
37
|
+
if (message.method === 'workspace/configuration') send({ id: message.id, result: [{}] });
|
|
38
|
+
else messages.push(message);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const wait = async predicate => {
|
|
42
|
+
const deadline = Date.now() + 15000;
|
|
43
|
+
while (Date.now() < deadline) {
|
|
44
|
+
const index = messages.findIndex(predicate);
|
|
45
|
+
if (index !== -1) return messages.splice(index, 1)[0];
|
|
46
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
47
|
+
}
|
|
48
|
+
assert.fail(`Timed out waiting for server: ${JSON.stringify(messages)}`);
|
|
49
|
+
};
|
|
50
|
+
const request = async (method, params) => {
|
|
51
|
+
const id = nextId++;
|
|
52
|
+
send({ id, method, params });
|
|
53
|
+
const message = await wait(message => message.id === id);
|
|
54
|
+
assert.equal(message.error, undefined);
|
|
55
|
+
return message.result;
|
|
56
|
+
};
|
|
57
|
+
const initialize = async () => {
|
|
58
|
+
await request('initialize', {
|
|
59
|
+
processId: process.pid,
|
|
60
|
+
capabilities: {},
|
|
61
|
+
workspaceFolders: [{ uri: pathToFileURL(cwd).href, name: 'lifecycle' }],
|
|
62
|
+
});
|
|
63
|
+
send({ method: 'initialized', params: {} });
|
|
64
|
+
await wait(message => message.method === NOTIFICATION_MODULE_GRAPH_BUILT);
|
|
65
|
+
};
|
|
66
|
+
return { child, send, wait, request, initialize };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
test('source updates refresh descriptors and notify clients with unchanged empty diagnostics', async t => {
|
|
70
|
+
const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), 'knip-ls-graph-update-')));
|
|
71
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
72
|
+
writeFileSync(path.join(cwd, 'package.json'), '{"name":"graph-update","type":"module"}');
|
|
73
|
+
writeFileSync(path.join(cwd, 'knip.json'), '{"entry":["index.js"],"project":["*.js"]}');
|
|
74
|
+
writeFileSync(path.join(cwd, 'index.js'), "import { used } from './value.js'; console.log(used);");
|
|
75
|
+
writeFileSync(path.join(cwd, 'value.js'), 'export const used = 1;');
|
|
76
|
+
const server = startServer(cwd, t);
|
|
77
|
+
await server.initialize();
|
|
78
|
+
const uri = pathToFileURL(path.join(cwd, 'value.js')).href;
|
|
79
|
+
const initial = await server.request(REQUEST_FILE_NODE, { uri });
|
|
80
|
+
const before = await server.request(REQUEST_RESULTS);
|
|
81
|
+
for (const issues of Object.values(before.issues)) assert.deepEqual(issues, {});
|
|
82
|
+
writeFileSync(path.join(cwd, 'value.js'), '\nexport const used = 1;');
|
|
83
|
+
server.send({ method: 'workspace/didChangeWatchedFiles', params: { changes: [{ uri, type: 2 }] } });
|
|
84
|
+
await server.wait(message => message.method === NOTIFICATION_MODULE_GRAPH_BUILT);
|
|
85
|
+
const updated = await server.request(REQUEST_FILE_NODE, { uri });
|
|
86
|
+
assert.equal(updated.exports[0].line, initial.exports[0].line + 1);
|
|
87
|
+
assert.deepEqual((await server.request(REQUEST_RESULTS)).issues, before.issues);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('older local Knip sessions keep the existing manifest reload fallback without exiting the server', async t => {
|
|
91
|
+
const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), 'knip-ls-legacy-')));
|
|
92
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
93
|
+
cpSync(
|
|
94
|
+
new URL('../../vscode-knip/test/fixtures/version-bump/knip', import.meta.url),
|
|
95
|
+
path.join(cwd, 'node_modules/knip'),
|
|
96
|
+
{
|
|
97
|
+
recursive: true,
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
writeFileSync(path.join(cwd, 'package.json'), '{"name":"legacy","type":"module"}');
|
|
101
|
+
writeFileSync(path.join(cwd, 'index.js'), '');
|
|
102
|
+
const server = startServer(cwd, t);
|
|
103
|
+
await server.initialize();
|
|
104
|
+
const before = await server.request(REQUEST_RESULTS);
|
|
105
|
+
assert.ok(before.issues.exports['index.js']['knip-version-1.0.0']);
|
|
106
|
+
writeFileSync(path.join(cwd, 'package.json'), '{"name":"legacy","type":"module","description":"updated"}');
|
|
107
|
+
server.send({
|
|
108
|
+
method: 'workspace/didChangeWatchedFiles',
|
|
109
|
+
params: { changes: [{ uri: pathToFileURL(path.join(cwd, 'package.json')).href, type: 2 }] },
|
|
110
|
+
});
|
|
111
|
+
await server.wait(message => message.method === NOTIFICATION_MODULE_GRAPH_BUILT);
|
|
112
|
+
assert.deepEqual((await server.request(REQUEST_RESULTS)).issues, before.issues);
|
|
113
|
+
assert.equal(server.child.exitCode, null);
|
|
114
|
+
});
|