zed-ets-language-server 2.3.4 → 3.1.0

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.
@@ -1,18 +1,36 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
2
  import { spawn } from 'child_process';
3
+ import { mkdtempSync } from 'fs';
4
+ import { tmpdir } from 'os';
3
5
  import { fileURLToPath } from 'url';
4
6
  import { dirname, join } from 'path';
5
7
 
6
8
  const __filename = fileURLToPath(import.meta.url);
7
9
  const __dirname = dirname(__filename);
8
10
 
11
+ const serverPath = join(__dirname, '../../index.js');
12
+ const mockServerPath = join(__dirname, '../mocks/mock-ets-server.js');
13
+ const bundledTsdk = join(__dirname, '../../node_modules/ohos-typescript/lib');
14
+
15
+ // Hermetic env: never inherit ambient tsdk/sdk settings from the shell.
16
+ function baseEnv(overrides = {}) {
17
+ return {
18
+ ...process.env,
19
+ ETS_LANG_SERVER: mockServerPath,
20
+ TSDK: '',
21
+ ZED_ETS_TSDK: '',
22
+ OHOS_SDK_PATH: '',
23
+ ZED_ETS_OHOS_SDK_PATH: '',
24
+ ...overrides,
25
+ };
26
+ }
27
+
9
28
  /**
10
29
  * 创建 LSP 消息
11
30
  */
12
31
  function createLSPMessage(content) {
13
- const json = JSON.stringify(content);
14
- const contentLength = Buffer.byteLength(json, 'utf8');
15
- return `Content-Length: ${contentLength}\r\n\r\n${json}`;
32
+ const contentLength = Buffer.byteLength(JSON.stringify(content));
33
+ return `Content-Length: ${contentLength}\r\n\r\n${JSON.stringify(content)}`;
16
34
  }
17
35
 
18
36
  /**
@@ -22,7 +40,7 @@ function parseLSPResponse(data) {
22
40
  const text = data.toString();
23
41
  const match = text.match(/Content-Length: (\d+)\r\n\r\n(.*)/s);
24
42
  if (!match) return null;
25
-
43
+
26
44
  try {
27
45
  return JSON.parse(match[2]);
28
46
  } catch (e) {
@@ -30,28 +48,77 @@ function parseLSPResponse(data) {
30
48
  }
31
49
  }
32
50
 
51
+ /**
52
+ * 等待特定响应
53
+ */
54
+ function waitForResponse(responses, predicate, timeout = 2000) {
55
+ return new Promise((resolve, reject) => {
56
+ const startTime = Date.now();
57
+ const checkInterval = setInterval(() => {
58
+ const response = responses.find(predicate);
59
+ if (response) {
60
+ clearInterval(checkInterval);
61
+ resolve(response);
62
+ } else if (Date.now() - startTime > timeout) {
63
+ clearInterval(checkInterval);
64
+ reject(new Error('Timeout waiting for response'));
65
+ }
66
+ }, 50);
67
+ });
68
+ }
69
+
70
+ function startWrapper(env) {
71
+ const serverProcess = spawn('node', [serverPath], {
72
+ stdio: ['pipe', 'pipe', 'pipe'],
73
+ env,
74
+ });
75
+
76
+ const responses = [];
77
+ let stdoutBuffer = Buffer.alloc(0);
78
+ serverProcess.stdout.on('data', (data) => {
79
+ stdoutBuffer = Buffer.concat([stdoutBuffer, data]);
80
+ // SDK discovery can make initialize responses span multiple pipe chunks.
81
+ while (true) {
82
+ const headerEnd = stdoutBuffer.indexOf('\r\n\r\n');
83
+ if (headerEnd < 0) return;
84
+ const header = stdoutBuffer.subarray(0, headerEnd).toString('ascii');
85
+ const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
86
+ if (!lengthMatch) throw new Error('Invalid LSP response header');
87
+ const bodyStart = headerEnd + 4;
88
+ const bodyEnd = bodyStart + Number(lengthMatch[1]);
89
+ if (stdoutBuffer.length < bodyEnd) return;
90
+ responses.push(JSON.parse(stdoutBuffer.subarray(bodyStart, bodyEnd).toString('utf8')));
91
+ stdoutBuffer = stdoutBuffer.subarray(bodyEnd);
92
+ }
93
+ });
94
+
95
+ serverProcess.stderr.on('data', (data) => {
96
+ console.error(`LSP Server Error: ${data}`);
97
+ });
98
+
99
+ return { serverProcess, responses };
100
+ }
101
+
102
+ function initializeMessage(id) {
103
+ return createLSPMessage({
104
+ jsonrpc: '2.0',
105
+ id,
106
+ method: 'initialize',
107
+ params: {
108
+ processId: null,
109
+ rootUri: null,
110
+ capabilities: {},
111
+ },
112
+ });
113
+ }
114
+
33
115
  describe('LSP Server Integration Tests', () => {
34
116
  let serverProcess;
35
117
  let responses = [];
118
+ let messageId = 1;
36
119
 
37
120
  beforeAll(() => {
38
- // 启动 LSP 服务器
39
- const serverPath = join(__dirname, '../../index.js');
40
- serverProcess = spawn('node', [serverPath], {
41
- stdio: ['pipe', 'pipe', 'pipe']
42
- });
43
-
44
- // 收集响应
45
- serverProcess.stdout.on('data', (data) => {
46
- const response = parseLSPResponse(data);
47
- if (response) {
48
- responses.push(response);
49
- }
50
- });
51
-
52
- serverProcess.stderr.on('data', (data) => {
53
- console.error(`LSP Server Error: ${data}`);
54
- });
121
+ ({ serverProcess, responses } = startWrapper(baseEnv()));
55
122
  });
56
123
 
57
124
  afterAll(() => {
@@ -60,65 +127,78 @@ describe('LSP Server Integration Tests', () => {
60
127
  }
61
128
  });
62
129
 
63
- it('should respond to initialize request', (done) => {
64
- const initRequest = {
65
- jsonrpc: '2.0',
66
- id: 1,
67
- method: 'initialize',
68
- params: {
69
- processId: process.pid,
70
- rootUri: null,
71
- capabilities: {}
72
- }
73
- };
130
+ it('should respond to initialize request', async () => {
131
+ serverProcess.stdin.write(initializeMessage(messageId));
132
+ const initResponse = await waitForResponse(responses, (r) => r.id === messageId);
133
+ messageId++;
134
+
135
+ expect(initResponse.result).toBeDefined();
136
+ expect(initResponse.result.capabilities).toBeDefined();
137
+ });
74
138
 
75
- const message = createLSPMessage(initRequest);
76
- serverProcess.stdin.write(message);
77
-
78
- // 等待响应
79
- setTimeout(() => {
80
- const initResponse = responses.find(r => r.id === 1);
81
- expect(initResponse).toBeDefined();
82
- expect(initResponse.result).toBeDefined();
83
- expect(initResponse.result.capabilities).toBeDefined();
84
- done();
85
- }, 1000);
139
+ it('should auto-detect the bundled tsdk when none is configured', async () => {
140
+ serverProcess.stdin.write(initializeMessage(messageId));
141
+ const initResponse = await waitForResponse(responses, (r) => r.id === messageId);
142
+ messageId++;
143
+
144
+ expect(initResponse.result.initializationOptions.typescript.tsdk).toBe(bundledTsdk);
86
145
  });
87
146
 
88
- it('should accept initialized notification', (done) => {
89
- const initializedNotif = {
147
+ it('should accept initialized notification', async () => {
148
+ serverProcess.stdin.write(createLSPMessage({
90
149
  jsonrpc: '2.0',
91
150
  method: 'initialized',
92
- params: {}
93
- };
94
-
95
- const message = createLSPMessage(initializedNotif);
96
- serverProcess.stdin.write(message);
151
+ params: {},
152
+ }));
97
153
 
98
154
  // 通知不需要响应,只需确保不崩溃
99
- setTimeout(() => {
100
- expect(serverProcess.killed).toBe(false);
101
- done();
102
- }, 500);
155
+ await new Promise((resolve) => setTimeout(resolve, 300));
156
+ expect(serverProcess.exitCode).toBeNull();
103
157
  });
104
158
 
105
- it('should handle shutdown request', (done) => {
106
- const shutdownRequest = {
159
+ it('should handle shutdown request', async () => {
160
+ serverProcess.stdin.write(createLSPMessage({
107
161
  jsonrpc: '2.0',
108
162
  id: 99,
109
163
  method: 'shutdown',
110
- params: null
111
- };
164
+ params: null,
165
+ }));
166
+
167
+ const shutdownResponse = await waitForResponse(responses, (r) => r.id === 99);
168
+ // 某些 LSP 服务器可能返回 null result
169
+ expect(shutdownResponse).toBeDefined();
170
+ });
171
+ });
172
+
173
+ describe('LSP tsdk fallback', () => {
174
+ // A tsdk without lib/typescript.js (e.g. the native TypeScript 7 line)
175
+ // used to hang the real server inside initialize. The wrapper must
176
+ // substitute the bundled ohos-typescript instead of forwarding it.
177
+ it('substitutes a broken TSDK env value with the bundled ohos-typescript', async () => {
178
+ const brokenTsdkDir = mkdtempSync(join(tmpdir(), 'zed-ets-broken-tsdk-'));
179
+ const { serverProcess, responses } = startWrapper(baseEnv({ TSDK: brokenTsdkDir }));
180
+
181
+ try {
182
+ serverProcess.stdin.write(initializeMessage(1));
183
+ const initResponse = await waitForResponse(responses, (r) => r.id === 1);
184
+
185
+ expect(initResponse.result.initializationOptions.typescript.tsdk).toBe(bundledTsdk);
186
+ } finally {
187
+ serverProcess.kill();
188
+ }
189
+ });
190
+
191
+ it('forwards a valid TSDK unchanged', async () => {
192
+ const { serverProcess, responses } = startWrapper(baseEnv({ TSDK: bundledTsdk }));
112
193
 
113
- const message = createLSPMessage(shutdownRequest);
114
- serverProcess.stdin.write(message);
194
+ try {
195
+ serverProcess.stdin.write(initializeMessage(1));
196
+ const initResponse = await waitForResponse(responses, (r) => r.id === 1);
115
197
 
116
- setTimeout(() => {
117
- const shutdownResponse = responses.find(r => r.id === 99);
118
- // 某些 LSP 服务器可能返回 null result
119
- expect(shutdownResponse).toBeDefined();
120
- done();
121
- }, 1000);
198
+ expect(initResponse.result.initializationOptions.typescript.tsdk).toBe(bundledTsdk);
199
+ } finally {
200
+ serverProcess.kill();
201
+ }
122
202
  });
123
203
  });
124
204
 
@@ -126,7 +206,7 @@ describe('LSP Message Protocol', () => {
126
206
  it('should format messages correctly', () => {
127
207
  const content = { jsonrpc: '2.0', method: 'test' };
128
208
  const message = createLSPMessage(content);
129
-
209
+
130
210
  expect(message).toContain('Content-Length:');
131
211
  expect(message).toContain('\r\n\r\n');
132
212
  expect(message).toContain(JSON.stringify(content));
@@ -140,7 +220,7 @@ describe('LSP Message Protocol', () => {
140
220
  };
141
221
  const data = createLSPMessage(mockResponse);
142
222
  const parsed = parseLSPResponse(Buffer.from(data));
143
-
223
+
144
224
  expect(parsed).toEqual(mockResponse);
145
225
  });
146
226
  });
@@ -0,0 +1,167 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const testDirectory = path.dirname(fileURLToPath(import.meta.url));
9
+ let fixtureDirectory;
10
+ let sdkRoot;
11
+ let hmsSdkRoot;
12
+ let serverProcess;
13
+ let stdoutBuffer = Buffer.alloc(0);
14
+ const responses = [];
15
+
16
+ function encodeLspMessage(message) {
17
+ const body = Buffer.from(JSON.stringify(message));
18
+ return Buffer.concat([
19
+ Buffer.from(`Content-Length: ${body.length}\r\n\r\n`),
20
+ body,
21
+ ]);
22
+ }
23
+
24
+ function collectLspMessages(chunk) {
25
+ stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
26
+
27
+ while (true) {
28
+ const headerEnd = stdoutBuffer.indexOf('\r\n\r\n');
29
+ if (headerEnd === -1) return;
30
+
31
+ const header = stdoutBuffer.subarray(0, headerEnd).toString('ascii');
32
+ const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
33
+ if (!lengthMatch) return;
34
+
35
+ const bodyStart = headerEnd + 4;
36
+ const bodyLength = Number(lengthMatch[1]);
37
+ if (stdoutBuffer.length < bodyStart + bodyLength) return;
38
+
39
+ const body = stdoutBuffer.subarray(bodyStart, bodyStart + bodyLength);
40
+ responses.push(JSON.parse(body.toString('utf8')));
41
+ stdoutBuffer = stdoutBuffer.subarray(bodyStart + bodyLength);
42
+ }
43
+ }
44
+
45
+ async function waitForResponse(predicate, timeout = 3000) {
46
+ const deadline = Date.now() + timeout;
47
+ while (Date.now() < deadline) {
48
+ const response = responses.find(predicate);
49
+ if (response) return response;
50
+ await new Promise((resolve) => setTimeout(resolve, 25));
51
+ }
52
+ throw new Error('Timed out waiting for LSP response');
53
+ }
54
+
55
+ beforeAll(async () => {
56
+ fixtureDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-deveco-'));
57
+ sdkRoot = path.join(fixtureDirectory, 'Contents', 'sdk', 'default', 'openharmony');
58
+ await fs.mkdir(path.join(sdkRoot, 'ets', 'api'), { recursive: true });
59
+ await fs.mkdir(path.join(sdkRoot, 'ets', 'kits'), { recursive: true });
60
+ await fs.mkdir(path.join(sdkRoot, 'ets', 'component'), { recursive: true });
61
+ await fs.mkdir(path.join(sdkRoot, 'ets', 'build-tools', 'ets-loader', 'declarations'), {
62
+ recursive: true,
63
+ });
64
+ await fs.writeFile(
65
+ path.join(sdkRoot, 'ets', 'build-tools', 'ets-loader', 'tsconfig.json'),
66
+ '{}\n',
67
+ );
68
+ await fs.writeFile(
69
+ path.join(sdkRoot, 'ets', 'kits', '@kit.AbilityKit.d.ts'),
70
+ 'export declare class UIAbility {}\n',
71
+ );
72
+ hmsSdkRoot = path.join(fixtureDirectory, 'Contents', 'sdk', 'default', 'hms');
73
+ await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'api'), { recursive: true });
74
+ await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'kits'), { recursive: true });
75
+ await fs.writeFile(
76
+ path.join(hmsSdkRoot, 'ets', 'kits', '@kit.TestHmsKit.d.ts'),
77
+ 'export declare const hmsApi: string;\n',
78
+ );
79
+ const placeholderSiblingHms = path.join(fixtureDirectory, 'hms');
80
+ await fs.mkdir(path.join(placeholderSiblingHms, 'ets', 'api'), { recursive: true });
81
+ await fs.mkdir(path.join(placeholderSiblingHms, 'ets', 'kits'), { recursive: true });
82
+
83
+ const wrapperPath = path.join(testDirectory, '..', '..', 'index.js');
84
+ const mockServerPath = path.join(testDirectory, '..', 'mocks', 'mock-ets-server.js');
85
+ const tsdk = path.join(testDirectory, '..', '..', 'node_modules', 'ohos-typescript', 'lib');
86
+ const env = { ...process.env };
87
+ delete env.ZED_ETS_OHOS_SDK_PATH;
88
+ delete env.OHOS_SDK_PATH;
89
+ delete env.HARMONYOS_SDK_HOME;
90
+ delete env.OPENHARMONY_SDK_HOME;
91
+ delete env.DEVECO_SDK_HOME;
92
+ delete env.ZED_ETS_HMS_SDK_PATH;
93
+ delete env.HMS_SDK_PATH;
94
+ Object.assign(env, {
95
+ ETS_LANG_SERVER: mockServerPath,
96
+ ZED_ETS_TSDK: tsdk,
97
+ DEVECO_STUDIO_HOME: fixtureDirectory,
98
+ TMPDIR: fixtureDirectory,
99
+ });
100
+
101
+ serverProcess = spawn(process.execPath, [wrapperPath], {
102
+ env,
103
+ stdio: ['pipe', 'pipe', 'pipe'],
104
+ });
105
+ serverProcess.stdout.on('data', collectLspMessages);
106
+ });
107
+
108
+ afterAll(async () => {
109
+ serverProcess?.kill();
110
+ await fs.rm(fixtureDirectory, { recursive: true, force: true });
111
+ });
112
+
113
+ describe('SDK initialization', () => {
114
+ it('injects the DevEco OpenHarmony SDK into ArkTS initialization', async () => {
115
+ serverProcess.stdin.write(
116
+ encodeLspMessage({
117
+ jsonrpc: '2.0',
118
+ id: 1,
119
+ method: 'initialize',
120
+ params: {
121
+ processId: process.pid,
122
+ rootUri: 'file:///tmp/harmony-project',
123
+ capabilities: {},
124
+ initializationOptions: {},
125
+ },
126
+ }),
127
+ );
128
+
129
+ const response = await waitForResponse((message) => message.id === 1);
130
+ const initializationOptions = response.result.initializationOptions;
131
+
132
+ expect(initializationOptions.ohos.sdkPath).toBe(sdkRoot);
133
+ expect(initializationOptions.ets.sdkPath).toBe(sdkRoot);
134
+ expect(initializationOptions.ohos.baseUrl).toBe(path.join(sdkRoot, 'ets'));
135
+ expect(initializationOptions.ets.hmsPath).toBe(hmsSdkRoot);
136
+ expect(initializationOptions.ohos.hmsSdkPath).toBe(hmsSdkRoot);
137
+ expect(initializationOptions.ohos.paths['*']).toContain(
138
+ path.join(hmsSdkRoot, 'ets', 'kits', '*'),
139
+ );
140
+ });
141
+
142
+ it('does not infer an HMS SDK from the placeholder OpenHarmony SDK', async () => {
143
+ serverProcess.stdin.write(
144
+ encodeLspMessage({
145
+ jsonrpc: '2.0',
146
+ id: 2,
147
+ method: 'initialize',
148
+ params: {
149
+ processId: process.pid,
150
+ rootUri: 'file:///tmp/harmony-project',
151
+ capabilities: {},
152
+ initializationOptions: {
153
+ ohosSdkPath: path.join(fixtureDirectory, 'invalid-openharmony'),
154
+ },
155
+ },
156
+ }),
157
+ );
158
+
159
+ const response = await waitForResponse((message) => message.id === 2);
160
+ const initializationOptions = response.result.initializationOptions;
161
+
162
+ expect(initializationOptions.ets.sdkPath).toBe(
163
+ path.join(fixtureDirectory, 'zed-ets-empty-ohos-sdk'),
164
+ );
165
+ expect(initializationOptions.ets.hmsPath).toBeUndefined();
166
+ });
167
+ });
@@ -91,10 +91,12 @@ struct Test {
91
91
  textDocumentSync: 1,
92
92
  documentFormattingProvider: true,
93
93
  documentRangeFormattingProvider: true
94
- }
94
+ },
95
+ // Echo what the wrapper forwarded so tests can assert on it.
96
+ initializationOptions: message.params?.initializationOptions ?? {}
95
97
  }
96
98
  };
97
-
99
+
98
100
  process.send(response);
99
101
  }
100
102