@knip/language-server 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,7 +108,8 @@ Optionally, the client can implement:
108
108
  - Show contention: ambiguous or shadowed exports behind re-exports
109
109
  - Show contention: converged (diamond-shaped) re-export paths
110
110
 
111
- Each `File.contention[name].sites` entry describes where contention occurs, its kind, and the originating bindings.
111
+ Each `File.contention[name].sites` entry describes where contention occurs, its
112
+ kind, and the originating bindings.
112
113
 
113
114
  Example:
114
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knip/language-server",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
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
- "vscode-languageserver": "^10.1.1",
22
- "vscode-languageserver-textdocument": "^1.0.14",
23
- "knip": "^6.36.0"
21
+ "knip": "^6.44.1",
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,11 @@ export class LanguageServer {
99
99
  /** @type {undefined | Session} */
100
100
  session;
101
101
 
102
+ revision = 0;
103
+
104
+ /** @type {undefined | number} */
105
+ failedRevision;
106
+
102
107
  /** @type {Rules} */
103
108
  rules = {};
104
109
 
@@ -152,8 +157,9 @@ export class LanguageServer {
152
157
  });
153
158
 
154
159
  this.connection.onInitialized(async () => {
160
+ const revision = this.revision;
155
161
  const config = await this.getConfig();
156
- if (config?.deferSession !== true) this.start();
162
+ if (revision === this.revision && config?.deferSession !== true) this.start();
157
163
  });
158
164
 
159
165
  this.connection.onRequest(REQUEST_START, () => this.start());
@@ -244,10 +250,13 @@ export class LanguageServer {
244
250
 
245
251
  async start() {
246
252
  if (this.session) return;
253
+ const revision = ++this.revision;
247
254
 
248
255
  try {
249
256
  const config = await this.getConfig();
257
+ if (revision !== this.revision) return;
250
258
  const knip = await this.#resolveKnipSession();
259
+ if (revision !== this.revision) return;
251
260
 
252
261
  const configFilePath = config?.configFilePath
253
262
  ? path.isAbsolute(config.configFilePath)
@@ -257,31 +266,39 @@ export class LanguageServer {
257
266
 
258
267
  this.connection.console.log('Creating options');
259
268
  const options = await knip.createOptions({ cwd: this.cwd, isSession: true, args: { config: configFilePath } });
269
+ if (revision !== this.revision) return;
260
270
  this.rules = options.rules;
261
271
 
262
272
  this.connection.console.log('Building module graph...');
263
273
  const start = Date.now();
264
274
  const session = await knip.createSession(options);
275
+ if (revision !== this.revision) return;
265
276
  this.connection.console.log(`Finished building module graph (${Date.now() - start}ms)`);
266
277
 
267
278
  this.session = session;
268
279
  this.publishDiagnostics(this.buildDiagnostics(session.getIssues().issues, config, this.rules));
269
280
  this.connection.sendNotification(NOTIFICATION_MODULE_GRAPH_BUILT, { duration: Date.now() - start });
270
281
  } catch (_error) {
282
+ if (revision !== this.revision) return;
283
+ this.failedRevision = revision;
271
284
  this.connection.console.error(`Error: ${_error}`);
272
285
  }
273
286
  }
274
287
 
275
288
  stop() {
289
+ this.revision++;
276
290
  this.session = undefined;
277
291
  this.fileCache = undefined;
292
+ this.packageJsonCache = undefined;
293
+ this.cycleDiagnostics.clear();
294
+ this.issuesByUri.clear();
278
295
  for (const uri of this.published) this.connection.sendDiagnostics({ uri, diagnostics: [] });
279
296
  this.published.clear();
280
297
  }
281
298
 
282
- restart() {
299
+ async restart() {
283
300
  this.stop();
284
- this.start();
301
+ await this.start();
285
302
  }
286
303
 
287
304
  getResults() {
@@ -296,7 +313,8 @@ export class LanguageServer {
296
313
  async handleFileChanges(params) {
297
314
  this.fileCache = undefined;
298
315
  this.packageJsonCache = undefined;
299
- if (!this.session) return;
316
+ const session = this.session;
317
+ if (!session && this.failedRevision !== this.revision) return;
300
318
 
301
319
  const cwd = this.cwd ?? process.cwd();
302
320
 
@@ -312,20 +330,26 @@ export class LanguageServer {
312
330
  changes.push({ type, filePath });
313
331
  }
314
332
 
315
- if (changes.length === 0) return;
333
+ if (!session || changes.length === 0) return;
316
334
 
317
335
  try {
318
- const result = await this.session.handleFileChanges(changes);
336
+ const result = await session.handleFileChanges(changes);
319
337
 
320
- if (!result) return;
338
+ if (!result || this.session !== session) return;
339
+ this.fileCache = undefined;
340
+ this.packageJsonCache = undefined;
341
+ this.cycleDiagnostics.clear();
321
342
 
322
343
  this.connection.console.log(
323
344
  `Module graph updated (${Math.floor(result.duration)}ms • ${(result.mem / 1024 / 1024).toFixed(2)}M)`
324
345
  );
325
346
 
326
347
  const config = await this.getConfig();
327
- this.publishDiagnostics(this.buildDiagnostics(this.session.getIssues().issues, config, this.rules));
348
+ if (this.session !== session) return;
349
+ this.publishDiagnostics(this.buildDiagnostics(session.getIssues().issues, config, this.rules));
350
+ this.connection.sendNotification(NOTIFICATION_MODULE_GRAPH_BUILT, { duration: result.duration });
328
351
  } catch (_error) {
352
+ if (this.session !== session) return;
329
353
  this.connection.console.error(`Error handling file changes: ${_error}`);
330
354
  }
331
355
  }
@@ -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,269 @@
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 changed = file => ({ changes: [{ uri: pathToFileURL(path.join(cwd, file)).href, type: 2 }] });
31
+ const updated = { duration: 1, mem: 1024 };
32
+ const config = { editor: { exports: { quickfix: { enabled: true } } } };
33
+ const options = { rules: { exports: 'warn' } };
34
+ const deferred = () => Promise.withResolvers();
35
+
36
+ const createSession = (name = 'current') => ({
37
+ getIssues: () => ({ issues: {} }),
38
+ getResults: () => ({ name }),
39
+ describeFile: () => ({ name, metrics: {} }),
40
+ describePackageJson: () => ({ dependenciesUsage: new Map([[name, { imports: [] }]]) }),
41
+ handleFileChanges: async () => updated,
42
+ });
43
+
44
+ const setup = t => {
45
+ t.mock.method(connection, 'listen', () => {});
46
+ t.mock.method(connection.workspace, 'getConfiguration', async () => config);
47
+ t.mock.method(connection.console, 'log', () => {});
48
+ t.mock.method(connection.console, 'error', () => {});
49
+ t.mock.method(connection, 'sendDiagnostics', () => {});
50
+ t.mock.method(connection, 'sendNotification', () => {});
51
+ t.mock.method(connection, 'onInitialized');
52
+ t.mock.method(knip, 'createOptions', async () => options);
53
+ t.mock.method(knip, 'createSession', async () => createSession());
54
+ const originalConsole = { ...console };
55
+ const server = new LanguageServer();
56
+ Object.assign(console, originalConsole);
57
+ server.cwd = cwd;
58
+ return server;
59
+ };
60
+
61
+ test('stop cancels automatic startup while initialization configuration is pending', async t => {
62
+ const server = setup(t);
63
+ const pending = deferred();
64
+ t.mock.method(server, 'getConfig', () => pending.promise);
65
+ const start = t.mock.method(server, 'start');
66
+ const initialized = connection.onInitialized.mock.calls[0].arguments[0]();
67
+ server.stop();
68
+ pending.resolve(config);
69
+ await initialized;
70
+ for (const call of start.mock.calls) await call.result;
71
+ assert.equal(server.getResults(), null);
72
+ assert.equal(connection.sendNotification.mock.callCount(), 0);
73
+ });
74
+
75
+ for (const phase of ['createOptions', 'createSession']) {
76
+ test(`stop discards a pending ${phase}`, { timeout: 3000 }, async t => {
77
+ const server = setup(t);
78
+ const entered = deferred();
79
+ const pending = deferred();
80
+ t.mock.method(knip, phase, () => {
81
+ entered.resolve();
82
+ return pending.promise;
83
+ });
84
+ const start = server.start();
85
+ await entered.promise;
86
+ server.stop();
87
+ pending.resolve(phase === 'createOptions' ? options : createSession('obsolete'));
88
+ await start;
89
+ assert.equal(server.getResults(), null);
90
+ assert.equal(server.getFileDescriptor(filePath), SESSION_LOADING);
91
+ assert.equal(connection.sendNotification.mock.callCount(), 0);
92
+ assert.equal(connection.sendDiagnostics.mock.callCount(), 0);
93
+ if (phase === 'createOptions') assert.equal(knip.createSession.mock.callCount(), 0);
94
+ });
95
+
96
+ test(`restart keeps the new session when obsolete ${phase} finishes last`, { timeout: 3000 }, async t => {
97
+ const server = setup(t);
98
+ const entered = deferred();
99
+ const pending = deferred();
100
+ let calls = 0;
101
+ t.mock.method(knip, phase, () => {
102
+ if (++calls > 1) return phase === 'createOptions' ? options : createSession();
103
+ entered.resolve();
104
+ return pending.promise;
105
+ });
106
+ const start = server.start();
107
+ await entered.promise;
108
+ await server.restart();
109
+ assert.deepEqual(server.getResults(), { name: 'current' });
110
+ pending.resolve(phase === 'createOptions' ? { rules: { exports: 'error' } } : createSession('obsolete'));
111
+ await start;
112
+ assert.deepEqual(server.getResults(), { name: 'current' });
113
+ assert.deepEqual(server.rules, options.rules);
114
+ assert.equal(connection.sendNotification.mock.callCount(), 1);
115
+ });
116
+ }
117
+
118
+ test('overlapping starts retain the latest session and ignore obsolete startup errors', { timeout: 3000 }, async t => {
119
+ const server = setup(t);
120
+ const entered = deferred();
121
+ const pending = deferred();
122
+ let calls = 0;
123
+ t.mock.method(knip, 'createSession', () => {
124
+ if (++calls > 1) return createSession();
125
+ entered.resolve();
126
+ return pending.promise;
127
+ });
128
+ const start = server.start();
129
+ await entered.promise;
130
+ await server.start();
131
+ pending.reject(new Error('obsolete startup failure'));
132
+ await start;
133
+ assert.deepEqual(server.getResults(), { name: 'current' });
134
+ assert.equal(connection.console.error.mock.callCount(), 0);
135
+ assert.equal(connection.sendNotification.mock.callCount(), 1);
136
+ });
137
+
138
+ for (const file of ['knip.json', 'package.json']) {
139
+ test(`${file} change restarts after a failed start`, { timeout: 3000 }, async t => {
140
+ const server = setup(t);
141
+ let calls = 0;
142
+ t.mock.method(knip, 'createOptions', async () => {
143
+ if (++calls === 1) throw new Error('invalid config');
144
+ return options;
145
+ });
146
+ await server.start();
147
+ assert.equal(server.getResults(), null);
148
+ assert.equal(connection.console.error.mock.callCount(), 1);
149
+ await server.handleFileChanges(changed(file));
150
+ assert.equal(calls, 2);
151
+ assert.deepEqual(server.getResults(), { name: 'current' });
152
+ assert.equal(connection.sendNotification.mock.callCount(), 1);
153
+ });
154
+ }
155
+
156
+ test('config change does not start a server stopped after a failed start', { timeout: 3000 }, async t => {
157
+ const server = setup(t);
158
+ let calls = 0;
159
+ t.mock.method(knip, 'createOptions', async () => {
160
+ calls++;
161
+ throw new Error('invalid config');
162
+ });
163
+ await server.start();
164
+ server.stop();
165
+ await server.handleFileChanges(changed('knip.json'));
166
+ assert.equal(calls, 1);
167
+ assert.equal(server.getResults(), null);
168
+ assert.equal(connection.sendNotification.mock.callCount(), 0);
169
+ });
170
+
171
+ test('source change after a failed start neither restarts nor updates', { timeout: 3000 }, async t => {
172
+ const server = setup(t);
173
+ let calls = 0;
174
+ t.mock.method(knip, 'createOptions', async () => {
175
+ calls++;
176
+ throw new Error('invalid config');
177
+ });
178
+ await server.start();
179
+ await server.handleFileChanges(changes);
180
+ assert.equal(calls, 1);
181
+ assert.equal(server.getResults(), null);
182
+ assert.equal(connection.console.error.mock.callCount(), 1);
183
+ });
184
+
185
+ test('graph updates invalidate descriptors requested during the update and notify without diagnostics', async t => {
186
+ const server = setup(t);
187
+ let session = createSession('before');
188
+ const pending = deferred();
189
+ server.session = {
190
+ ...session,
191
+ describeFile: () => session.describeFile(),
192
+ describePackageJson: () => session.describePackageJson(),
193
+ handleFileChanges: () => pending.promise,
194
+ };
195
+ const update = server.handleFileChanges(changes);
196
+ assert.equal(server.getFileDescriptor(filePath).name, 'before');
197
+ assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['before']);
198
+ session = createSession('after');
199
+ pending.resolve(updated);
200
+ await update;
201
+ assert.equal(server.getFileDescriptor(filePath).name, 'after');
202
+ assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['after']);
203
+ assert.equal(connection.sendDiagnostics.mock.callCount(), 0);
204
+ assert.deepEqual(connection.sendNotification.mock.calls[0]?.arguments, [
205
+ NOTIFICATION_MODULE_GRAPH_BUILT,
206
+ { duration: updated.duration },
207
+ ]);
208
+ });
209
+
210
+ test('stop clears package descriptors before another session starts', async t => {
211
+ const server = setup(t);
212
+ server.session = createSession('obsolete');
213
+ assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['obsolete']);
214
+ server.stop();
215
+ await server.start();
216
+ assert.deepEqual(Object.keys(server.getPackageJsonDescriptor().dependenciesUsage), ['current']);
217
+ });
218
+
219
+ for (const replaced of [false, true]) {
220
+ for (const phase of ['update', 'configuration']) {
221
+ test(`discard an obsolete update awaiting ${phase} after ${replaced ? 'replacement' : 'stop'}`, async t => {
222
+ const server = setup(t);
223
+ const pending = deferred();
224
+ const entered = deferred();
225
+ const old = createSession('obsolete');
226
+ server.session = old;
227
+ if (phase === 'update') old.handleFileChanges = () => pending.promise;
228
+ else {
229
+ t.mock.method(server, 'getConfig', () => {
230
+ entered.resolve();
231
+ return pending.promise;
232
+ });
233
+ }
234
+ const update = server.handleFileChanges(changes);
235
+ if (phase === 'configuration') await entered.promise;
236
+ server.stop();
237
+ if (replaced) {
238
+ server.session = createSession();
239
+ server.session.getIssues = () => ({ issues: { files: { [filePath]: { file: { type: 'files', filePath } } } } });
240
+ server.getFileDescriptor(filePath);
241
+ server.getPackageJsonDescriptor();
242
+ }
243
+ const file = server.fileCache;
244
+ const manifest = server.packageJsonCache;
245
+ pending.resolve(phase === 'update' ? updated : config);
246
+ await update;
247
+ assert.equal(server.fileCache, file);
248
+ assert.equal(server.packageJsonCache, manifest);
249
+ assert.equal(connection.sendDiagnostics.mock.callCount(), 0);
250
+ assert.equal(connection.sendNotification.mock.callCount(), 0);
251
+ assert.equal(connection.console.error.mock.callCount(), 0);
252
+ });
253
+ }
254
+ }
255
+
256
+ test('obsolete update errors cannot affect the replacement session', async t => {
257
+ const server = setup(t);
258
+ await server.start();
259
+ const pending = deferred();
260
+ server.session.handleFileChanges = () => pending.promise;
261
+ const update = server.handleFileChanges(changes);
262
+ await server.restart();
263
+ pending.reject(new Error('obsolete update failure'));
264
+ await update;
265
+ assert.deepEqual(server.getResults(), { name: 'current' });
266
+ assert.deepEqual(server.rules, options.rules);
267
+ assert.equal(connection.console.error.mock.callCount(), 0);
268
+ assert.equal(connection.sendNotification.mock.callCount(), 2);
269
+ });
@@ -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
+ });