@knip/language-server 3.1.0 → 3.1.1

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
@@ -105,8 +105,10 @@ with direct links to their usage locations.
105
105
  Optionally, the client can implement:
106
106
 
107
107
  - Follow cursor between open document and highlight export in view
108
- - Show contention: naming conflicts through re-exports
109
- - Show contention: branched/diamond-shaped re-export structures
108
+ - Show contention: ambiguous or shadowed exports behind re-exports
109
+ - Show contention: converged (diamond-shaped) re-export paths
110
+
111
+ Each `File.contention[name].sites` entry describes where contention occurs, its kind, and the originating bindings.
110
112
 
111
113
  Example:
112
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knip/language-server",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/webpro-nl/knip.git",
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "vscode-languageserver": "^10.1.1",
22
22
  "vscode-languageserver-textdocument": "^1.0.14",
23
- "knip": "^6.35.0"
23
+ "knip": "^6.36.0"
24
24
  },
25
25
  "engines": {
26
26
  "node": "^20.19.0 || >=22.12.0"
@@ -40,6 +40,15 @@ export const createRemoveDependencyEdit = (document, uri, issue) => {
40
40
  const range = { start: { line: lineIndex, character: 0 }, end: { line: lineIndex + 1, character: 0 } };
41
41
  const edits = [TextEdit.del(range)];
42
42
 
43
+ if (lineIndex > 0 && !document.getText(range).trimEnd().endsWith(',')) {
44
+ const prevRange = { start: { line: lineIndex - 1, character: 0 }, end: { line: lineIndex, character: 0 } };
45
+ const prevLine = document.getText(prevRange).trimEnd();
46
+ if (prevLine.endsWith(',')) {
47
+ const start = { line: lineIndex - 1, character: prevLine.length - 1 };
48
+ edits.push(TextEdit.del({ start, end: { line: lineIndex - 1, character: prevLine.length } }));
49
+ }
50
+ }
51
+
43
52
  return { changes: { [uri]: edits } };
44
53
  } catch (_error) {
45
54
  return null;
package/src/server.js CHANGED
@@ -237,7 +237,7 @@ export class LanguageServer {
237
237
  } catch {}
238
238
  }
239
239
  this.connection.console.log(
240
- `Using bundled knip${readKnipVersion(createRequire(__filename).resolve('knip/session'))}`
240
+ `Using bundled knip${readKnipVersion(createRequire(import.meta.url).resolve('knip/session'))}`
241
241
  );
242
242
  return { createOptions, createSession };
243
243
  }
@@ -0,0 +1,55 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { TextDocument } from 'vscode-languageserver-textdocument';
4
+ import { createRemoveDependencyEdit } from '../src/code-actions.js';
5
+
6
+ const uri = 'file:///project/package.json';
7
+
8
+ const manifest = (eol = '\n') =>
9
+ [
10
+ '{',
11
+ ' "name": "project",',
12
+ ' "dependencies": {',
13
+ ' "react": "^19.0.0",',
14
+ ' "lodash": "^4.17.21",',
15
+ ' "chalk": "^5.0.0"',
16
+ ' },',
17
+ ' "devDependencies": {',
18
+ ' "typescript": "^5.0.0"',
19
+ ' }',
20
+ '}',
21
+ '',
22
+ ].join(eol);
23
+
24
+ const remove = (text, symbol) => {
25
+ const document = TextDocument.create(uri, 'json', 1, text);
26
+ const line = text.split(/\r?\n/).findIndex(l => l.includes(`"${symbol}"`)) + 1;
27
+ const edit = createRemoveDependencyEdit(document, uri, { type: 'dependencies', symbol, line });
28
+ return TextDocument.applyEdits(document, edit.changes[uri]);
29
+ };
30
+
31
+ for (const [label, eol] of [
32
+ ['LF', '\n'],
33
+ ['CRLF', '\r\n'],
34
+ ]) {
35
+ test(`removes first dependency (${label})`, () => {
36
+ const result = remove(manifest(eol), 'react');
37
+ assert.deepEqual(JSON.parse(result).dependencies, { lodash: '^4.17.21', chalk: '^5.0.0' });
38
+ });
39
+
40
+ test(`removes middle dependency (${label})`, () => {
41
+ const result = remove(manifest(eol), 'lodash');
42
+ assert.deepEqual(JSON.parse(result).dependencies, { react: '^19.0.0', chalk: '^5.0.0' });
43
+ });
44
+
45
+ test(`removes last dependency (${label})`, () => {
46
+ const result = remove(manifest(eol), 'chalk');
47
+ assert.deepEqual(JSON.parse(result).dependencies, { react: '^19.0.0', lodash: '^4.17.21' });
48
+ assert.ok(result.includes(`"lodash": "^4.17.21"${eol} }`));
49
+ });
50
+
51
+ test(`removes sole dependency (${label})`, () => {
52
+ const result = remove(manifest(eol), 'typescript');
53
+ assert.deepEqual(JSON.parse(result).devDependencies, {});
54
+ });
55
+ }
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawn } from 'node:child_process';
3
+ import { mkdtempSync, 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 } from '../src/constants.js';
9
+
10
+ const cli = fileURLToPath(new URL('../src/cli.js', import.meta.url));
11
+
12
+ const encode = payload => {
13
+ const body = JSON.stringify({ jsonrpc: '2.0', ...payload });
14
+ return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
15
+ };
16
+
17
+ test('falls back to bundled knip when the project has no local knip', async () => {
18
+ const cwd = mkdtempSync(path.join(tmpdir(), 'knip-ls-'));
19
+ writeFileSync(path.join(cwd, 'package.json'), '{"name":"standalone","main":"index.js"}');
20
+ writeFileSync(path.join(cwd, 'index.js'), '');
21
+
22
+ const server = spawn(process.execPath, [cli, '--stdio'], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
23
+ const logs = [];
24
+ let buffer = '';
25
+
26
+ const done = new Promise((resolve, reject) => {
27
+ const timer = setTimeout(() => reject(new Error(`Timeout. Logs:\n${logs.join('\n')}`)), 30_000);
28
+ server.stdout.on('data', chunk => {
29
+ buffer += chunk;
30
+ for (;;) {
31
+ const match = buffer.match(/Content-Length: (\d+)\r\n\r\n/);
32
+ if (!match || buffer.length < match.index + match[0].length + Number(match[1])) break;
33
+ const start = match.index + match[0].length;
34
+ const msg = JSON.parse(buffer.slice(start, start + Number(match[1])));
35
+ buffer = buffer.slice(start + Number(match[1]));
36
+ if (msg.method === 'window/logMessage') logs.push(msg.params.message);
37
+ if (msg.method === 'workspace/configuration') server.stdin.write(encode({ id: msg.id, result: [{}] }));
38
+ if (msg.method === NOTIFICATION_MODULE_GRAPH_BUILT) {
39
+ clearTimeout(timer);
40
+ resolve();
41
+ }
42
+ }
43
+ });
44
+ });
45
+
46
+ server.stdin.write(
47
+ encode({
48
+ id: 1,
49
+ method: 'initialize',
50
+ params: {
51
+ processId: process.pid,
52
+ capabilities: {},
53
+ workspaceFolders: [{ uri: pathToFileURL(cwd).href, name: 'standalone' }],
54
+ },
55
+ })
56
+ );
57
+ server.stdin.write(encode({ method: 'initialized', params: {} }));
58
+
59
+ try {
60
+ await done;
61
+ } finally {
62
+ server.kill();
63
+ rmSync(cwd, { recursive: true, force: true });
64
+ }
65
+
66
+ assert.ok(
67
+ logs.some(log => log.startsWith('Using bundled knip')),
68
+ logs.join('\n')
69
+ );
70
+ assert.ok(!logs.some(log => log.includes('ReferenceError')), logs.join('\n'));
71
+ });