zed-ets-language-server 1.1.0 → 2.0.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.
package/index.js CHANGED
@@ -2,82 +2,14 @@
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
5
+ import { logger } from './lib/logger.js';
6
+ import { parse } from './lib/data-parser.js';
7
+ import { listHelperPaths } from './lib/lib-expander.js'
10
8
 
11
9
  // ETS language server path, passed by Rust extension process through environment variable
12
10
  const etsLangServerPath = process.env.ETS_LANG_SERVER;
13
11
 
14
- function createSimpleLogger() {
15
- if (process.env.ZED_ETS_LANG_SERVER_LOG !== 'true') {
16
- const noop = (_msg) => {};
17
- return {
18
- info: noop,
19
- success: noop,
20
- error: console.error,
21
- warn: noop,
22
- section: noop,
23
- data: noop,
24
- close: noop,
25
- };
26
- }
27
- // Create log file write stream
28
- const logFilePath = path.join(__dirname, 'arkts-lsw.log');
29
- const logStream = fs.createWriteStream(logFilePath, { flags: 'w+' });
30
-
31
- // Function to get current timestamp
32
- const getTimestamp = () => {
33
- const now = new Date();
34
- return now.toISOString().slice(0, 19).replace('T', ' ');
35
- };
36
-
37
- // Logging utility
38
- const logger = {
39
- info: (msg) => {
40
- const timestamp = getTimestamp();
41
- const logMsg = `[${timestamp}] ℹ ${msg}\n`;
42
- logStream.write(logMsg);
43
- },
44
- success: (msg) => {
45
- const timestamp = getTimestamp();
46
- const logMsg = `[${timestamp}] ✓ ${msg}\n`;
47
- logStream.write(logMsg);
48
- },
49
- error: (msg) => {
50
- const timestamp = getTimestamp();
51
- const logMsg = `[${timestamp}] ✗ ${msg}\n`;
52
- logStream.write(logMsg);
53
- process.stderr.write(logMsg);
54
- },
55
- warn: (msg) => {
56
- const timestamp = getTimestamp();
57
- const logMsg = `[${timestamp}] ⚠ ${msg}\n`;
58
- logStream.write(logMsg);
59
- },
60
- section: (msg) => {
61
- const timestamp = getTimestamp();
62
- const logMsg = `\n[${timestamp}] ${msg}\n\n`;
63
- logStream.write(logMsg);
64
- },
65
- data: (label, data) => {
66
- const timestamp = getTimestamp();
67
- const logMsg = `[${timestamp}] ${label}: ${JSON.stringify(data, null, 2)}\n`;
68
- logStream.write(logMsg);
69
- },
70
- // Add method to close log stream
71
- close: () => {
72
- logStream.end();
73
- },
74
- };
75
-
76
- return logger;
77
- }
78
-
79
12
  async function main() {
80
- const logger = createSimpleLogger();
81
13
  logger.section('🚀 ETS Language Server Wrapper');
82
14
 
83
15
  // Check if language server exists
@@ -116,55 +48,43 @@ async function main() {
116
48
  });
117
49
 
118
50
  // Set up forwarding of process.stdin to serverProcess IPC
119
- let stdinBuffer = '';
120
- process.stdin.on('data', (data) => {
121
- stdinBuffer += data.toString();
122
-
123
- while (true) {
124
- // Find Content-Length header
125
- const lengthMatch = stdinBuffer.match(/Content-Length: (\d+)\r\n/);
126
- if (!lengthMatch) break;
127
-
128
- const contentLength = Number.parseInt(lengthMatch[1]);
129
- const headerEnd = stdinBuffer.indexOf('\r\n\r\n');
130
-
131
- if (headerEnd === -1) break;
132
-
133
- const messageStart = headerEnd + 4;
134
- const messageEnd = messageStart + contentLength;
135
-
136
- if (stdinBuffer.length < messageEnd) break;
137
-
138
- // Extract message
139
- const messageJson = stdinBuffer.substring(messageStart, messageEnd);
140
- stdinBuffer = stdinBuffer.substring(messageEnd);
141
-
142
- try {
143
- const message = JSON.parse(messageJson);
144
- // Send message to language server via IPC
145
- serverProcess.send(message);
146
-
147
- // This special ets request is required in document: https://github.com/ohosvscode/arkTS/tree/next/packages/language-server
148
- // When this goes wrong, ETS UI decorators and functions will be type of any
149
- if (message.method === 'initialize') {
150
- const { initializationOptions } = message.params;
151
- const etsSpecialRequest = {
152
- jsonrpc: '2.0',
153
- id: Date.now(),
154
- method: 'ets/waitForEtsConfigurationChangedRequested',
155
- params: {
156
- typescript: initializationOptions.typescript,
157
- ohos: initializationOptions.ohos,
158
- debug: initializationOptions.debug,
159
- },
160
- };
161
- serverProcess.send(etsSpecialRequest);
162
- }
163
- } catch (error) {
164
- logger.error(`Failed to parse message from stdin: ${error.message}, ${messageJson}`);
165
- }
51
+ process.stdin.setEncoding('utf8');
52
+ process.stdin.on('data', (data) => parse(data, async (message) => {
53
+ // This special ets request is required in document: https://github.com/ohosvscode/arkTS/tree/next/packages/language-server
54
+ // When this goes wrong, ETS UI decorators and functions will be type of any
55
+ if (message.method === 'initialize') {
56
+ const { initializationOptions } = message.params;
57
+
58
+ const ohos = await listHelperPaths(initializationOptions.tsdk, initializationOptions.ohosSdkPath);
59
+
60
+ const etsSpecialRequest = {
61
+ jsonrpc: '2.0',
62
+ id: Date.now(),
63
+ method: 'ets/waitForEtsConfigurationChangedRequested',
64
+ params: {
65
+ typescript: {
66
+ tsdk: initializationOptions.tsdk,
67
+ },
68
+ ohos: ohos,
69
+ },
70
+ };
71
+
72
+ const generalInitRequest = message;
73
+ generalInitRequest.params.initializationOptions.typescript = {
74
+ tsdk: initializationOptions.tsdk,
75
+ };
76
+ generalInitRequest.params.initializationOptions.ohos = ohos;
77
+
78
+ logger.info(JSON.stringify(generalInitRequest));
79
+ logger.info(JSON.stringify(etsSpecialRequest));
80
+
81
+ serverProcess.send(generalInitRequest);
82
+ serverProcess.send(etsSpecialRequest);
83
+ return;
166
84
  }
167
- });
85
+ // Send message to language server via IPC
86
+ serverProcess.send(message);
87
+ }));
168
88
 
169
89
  // Error handling
170
90
  process.on('SIGTERM', () => {
@@ -0,0 +1,38 @@
1
+ import { logger } from './logger.js';
2
+
3
+ let stdinBuffer = '';
4
+
5
+ export function parse(data, callback) {
6
+ stdinBuffer += data.toString();
7
+
8
+ while (true) {
9
+ // Find Content-Length header
10
+ const lengthMatch = stdinBuffer.match(/Content-Length: (\d+)\r\n/);
11
+ if (!lengthMatch) break;
12
+
13
+ const contentLength = Number.parseInt(lengthMatch[1]);
14
+ const headerEnd = stdinBuffer.indexOf('\r\n\r\n');
15
+
16
+ if (headerEnd === -1) break;
17
+
18
+ const messageStart = headerEnd + 4;
19
+ const messageEnd = messageStart + contentLength;
20
+
21
+ if (stdinBuffer.length < messageEnd) break;
22
+
23
+ // Extract message
24
+ const messageJson = stdinBuffer.substring(messageStart, messageEnd);
25
+ stdinBuffer = stdinBuffer.substring(messageEnd);
26
+
27
+ try {
28
+ const message = JSON.parse(messageJson);
29
+ callback(message);
30
+ } catch (error) {
31
+ logger.error(`Error parsing message: ${error.message} ${error.stack}`);
32
+ }
33
+ }
34
+ }
35
+
36
+ export function clearBuffer() {
37
+ stdinBuffer = '';
38
+ }
@@ -0,0 +1,279 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { parse, clearBuffer } from './data-parser.js';
3
+
4
+ // Mock the logger module
5
+ vi.mock('./logger.js', () => ({
6
+ logger: {
7
+ error: vi.fn(),
8
+ },
9
+ }));
10
+
11
+ describe('data-parser', () => {
12
+ beforeEach(() => {
13
+ // Clear buffer before each test
14
+ clearBuffer();
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ afterEach(() => {
19
+ // Clear buffer after each test
20
+ clearBuffer();
21
+ });
22
+
23
+ describe('parse', () => {
24
+ it('should parse a complete message with Content-Length header', (done) => {
25
+ const message = { jsonrpc: '2.0', method: 'initialize', id: 1 };
26
+ const messageJson = JSON.stringify(message);
27
+ const data = Buffer.from(`Content-Length: ${messageJson.length}\r\n\r\n${messageJson}`);
28
+
29
+ const callback = vi.fn((parsedMessage) => {
30
+ expect(parsedMessage).toEqual(message);
31
+ expect(callback).toHaveBeenCalledTimes(1);
32
+ done();
33
+ });
34
+
35
+ parse(data, callback);
36
+ });
37
+
38
+ it('should handle multiple messages in one buffer', () => {
39
+ const message1 = { jsonrpc: '2.0', method: 'initialize', id: 1 };
40
+ const message2 = { jsonrpc: '2.0', method: 'initialized' };
41
+
42
+ const messageJson1 = JSON.stringify(message1);
43
+ const messageJson2 = JSON.stringify(message2);
44
+
45
+ const data = Buffer.from(
46
+ `Content-Length: ${messageJson1.length}\r\n\r\n${messageJson1}` +
47
+ `Content-Length: ${messageJson2.length}\r\n\r\n${messageJson2}`
48
+ );
49
+
50
+ const callback = vi.fn();
51
+ parse(data, callback);
52
+
53
+ expect(callback).toHaveBeenCalledTimes(2);
54
+ expect(callback).toHaveBeenNthCalledWith(1, message1);
55
+ expect(callback).toHaveBeenNthCalledWith(2, message2);
56
+ });
57
+
58
+ it('should handle partial messages across multiple parse calls', (done) => {
59
+ const message = { jsonrpc: '2.0', method: 'initialize', id: 1 };
60
+ const messageJson = JSON.stringify(message);
61
+
62
+ // Split the data into two parts
63
+ const headerPart = `Content-Length: ${messageJson.length}\r\n\r\n`;
64
+ const messagePart = messageJson;
65
+
66
+ const callback = vi.fn((parsedMessage) => {
67
+ expect(parsedMessage).toEqual(message);
68
+ expect(callback).toHaveBeenCalledTimes(1);
69
+ done();
70
+ });
71
+
72
+ // First call with just the header
73
+ parse(Buffer.from(headerPart), callback);
74
+ expect(callback).not.toHaveBeenCalled();
75
+
76
+ // Second call with the message
77
+ parse(Buffer.from(messagePart), callback);
78
+ });
79
+
80
+ it('should handle message with additional headers', (done) => {
81
+ const message = { jsonrpc: '2.0', method: 'initialize', id: 1 };
82
+ const messageJson = JSON.stringify(message);
83
+ const data = Buffer.from(
84
+ `Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n` +
85
+ `Content-Length: ${messageJson.length}\r\n\r\n${messageJson}`
86
+ );
87
+
88
+ const callback = vi.fn((parsedMessage) => {
89
+ expect(parsedMessage).toEqual(message);
90
+ expect(callback).toHaveBeenCalledTimes(1);
91
+ done();
92
+ });
93
+
94
+ parse(data, callback);
95
+ });
96
+
97
+ it('should ignore incomplete messages and wait for more data', () => {
98
+ const message = { jsonrpc: '2.0', method: 'initialize', id: 1 };
99
+ const messageJson = JSON.stringify(message);
100
+
101
+ // Send header but not the complete message
102
+ const incompleteData = Buffer.from(
103
+ `Content-Length: ${messageJson.length}\r\n\r\n${messageJson.substring(0, 10)}`
104
+ );
105
+
106
+ const callback = vi.fn();
107
+ parse(incompleteData, callback);
108
+
109
+ expect(callback).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('should ignore data without Content-Length header', () => {
113
+ const data = Buffer.from('Some random data without proper headers');
114
+ const callback = vi.fn();
115
+
116
+ parse(data, callback);
117
+
118
+ expect(callback).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it('should ignore data with invalid Content-Length', () => {
122
+ const data = Buffer.from('Content-Length: invalid\r\n\r\n{}');
123
+ const callback = vi.fn();
124
+
125
+ parse(data, callback);
126
+
127
+ expect(callback).not.toHaveBeenCalled();
128
+ });
129
+
130
+ it('should ignore data with negative Content-Length', () => {
131
+ const data = Buffer.from('Content-Length: -10\r\n\r\n{}');
132
+ const callback = vi.fn();
133
+
134
+ parse(data, callback);
135
+
136
+ expect(callback).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it('should ignore data with Content-Length header but no message terminator', () => {
140
+ const data = Buffer.from('Content-Length: 2\r\n');
141
+ const callback = vi.fn();
142
+
143
+ parse(data, callback);
144
+
145
+ expect(callback).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it('should handle JSON parsing errors gracefully', async () => {
149
+ const invalidJson = '{ invalid json }';
150
+ const data = Buffer.from(`Content-Length: ${invalidJson.length}\r\n\r\n${invalidJson}`);
151
+
152
+ const { logger } = await import('./logger.js');
153
+ const callback = vi.fn();
154
+
155
+ parse(data, callback);
156
+
157
+ expect(callback).not.toHaveBeenCalled();
158
+ expect(logger.error).toHaveBeenCalledWith(
159
+ expect.stringContaining('Error parsing message:')
160
+ );
161
+ });
162
+
163
+ it('should handle empty message content', () => {
164
+ const data = Buffer.from('Content-Length: 0\r\n\r\n');
165
+ const callback = vi.fn();
166
+
167
+ parse(data, callback);
168
+
169
+ expect(callback).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('should preserve buffer state across multiple parse calls', () => {
173
+ const message1 = { jsonrpc: '2.0', method: 'initialize', id: 1 };
174
+ const messageJson1 = JSON.stringify(message1);
175
+
176
+ // First, send partial data
177
+ const partialHeader = 'Content-Length: ';
178
+ parse(Buffer.from(partialHeader), () => {});
179
+
180
+ // Then send the rest
181
+ const restOfData = `${messageJson1.length}\r\n\r\n${messageJson1}`;
182
+ const callback = vi.fn();
183
+ parse(Buffer.from(restOfData), callback);
184
+
185
+ expect(callback).toHaveBeenCalledWith(message1);
186
+ });
187
+
188
+ it('should handle complex JSON messages with nested objects', (done) => {
189
+ const message = {
190
+ jsonrpc: '2.0',
191
+ method: 'textDocument/completion',
192
+ params: {
193
+ textDocument: { uri: 'file:///test.ets' },
194
+ position: { line: 10, character: 5 },
195
+ context: { triggerKind: 1 }
196
+ },
197
+ id: 2
198
+ };
199
+
200
+ const messageJson = JSON.stringify(message);
201
+ const data = Buffer.from(`Content-Length: ${messageJson.length}\r\n\r\n${messageJson}`);
202
+
203
+ const callback = vi.fn((parsedMessage) => {
204
+ expect(parsedMessage).toEqual(message);
205
+ expect(callback).toHaveBeenCalledTimes(1);
206
+ done();
207
+ });
208
+
209
+ parse(data, callback);
210
+ });
211
+
212
+ it('should handle Unicode characters in messages', (done) => {
213
+ const message = {
214
+ jsonrpc: '2.0',
215
+ method: 'textDocument/publishDiagnostics',
216
+ params: {
217
+ uri: 'file:///test.ets',
218
+ diagnostics: [{
219
+ message: '测试消息 with émojis 🚀',
220
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } }
221
+ }]
222
+ }
223
+ };
224
+
225
+ const messageJson = JSON.stringify(message);
226
+ const data = Buffer.from(`Content-Length: ${messageJson.length}\r\n\r\n${messageJson}`);
227
+
228
+ const callback = vi.fn((parsedMessage) => {
229
+ expect(parsedMessage).toEqual(message);
230
+ expect(callback).toHaveBeenCalledTimes(1);
231
+ done();
232
+ });
233
+
234
+ parse(data, callback);
235
+ });
236
+ });
237
+
238
+ describe('clearBuffer', () => {
239
+ it('should reset the internal buffer to empty', () => {
240
+ // First add some data to the buffer
241
+ const partialData = Buffer.from('Content-Length: 50\r\n');
242
+ parse(partialData, () => {});
243
+
244
+ // Clear the buffer
245
+ clearBuffer();
246
+
247
+ // Try to parse again - should not process any data
248
+ const message = { jsonrpc: '2.0', method: 'initialize', id: 1 };
249
+ const messageJson = JSON.stringify(message);
250
+ const completeData = Buffer.from(`\r\n\r\n${messageJson}`);
251
+ const callback = vi.fn();
252
+
253
+ parse(completeData, callback);
254
+ expect(callback).not.toHaveBeenCalled();
255
+ });
256
+
257
+ it('should allow fresh parsing after clearing buffer', (done) => {
258
+ // Add some incomplete data
259
+ const incompleteData = Buffer.from('Content-Length: 10\r\n');
260
+ parse(incompleteData, () => {});
261
+
262
+ // Clear buffer
263
+ clearBuffer();
264
+
265
+ // Now send a complete message
266
+ const message = { jsonrpc: '2.0', method: 'initialize', id: 1 };
267
+ const messageJson = JSON.stringify(message);
268
+ const completeData = Buffer.from(`Content-Length: ${messageJson.length}\r\n\r\n${messageJson}`);
269
+
270
+ const callback = vi.fn((parsedMessage) => {
271
+ expect(parsedMessage).toEqual(message);
272
+ expect(callback).toHaveBeenCalledTimes(1);
273
+ done();
274
+ });
275
+
276
+ parse(completeData, callback);
277
+ });
278
+ });
279
+ });
@@ -0,0 +1,94 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * 根据提供的路径,返回其中所有符合正则要求文件名的文件名列表
6
+ * @param {string} dirPath - 要搜索的目录路径
7
+ * @param {RegExp} pattern - 用于匹配文件名的正则表达式
8
+ * @param {boolean} recursive - 是否递归搜索子目录,默认为 true
9
+ * @returns {Promise<string[]>} 符合条件的文件名列表的 Promise
10
+ */
11
+ export async function getFilesByPattern(dirPath, pattern, recursive = true) {
12
+ const result = [];
13
+
14
+ try {
15
+ // 检查路径是否存在
16
+ try {
17
+ await fs.access(dirPath);
18
+ } catch (error) {
19
+ console.warn(`路径不存在: ${dirPath}`);
20
+ return result;
21
+ }
22
+
23
+ // 检查是否为目录
24
+ const stat = await fs.stat(dirPath);
25
+ if (!stat.isDirectory()) {
26
+ console.warn(`路径不是目录: ${dirPath}`);
27
+ return result;
28
+ }
29
+
30
+ // 读取目录内容
31
+ const items = await fs.readdir(dirPath);
32
+
33
+ // 使用 Promise.all 并行处理所有项目
34
+ const promises = items.map(async (item) => {
35
+ const fullPath = path.join(dirPath, item);
36
+
37
+ try {
38
+ const itemStat = await fs.stat(fullPath);
39
+
40
+ if (itemStat.isDirectory() && recursive) {
41
+ // 递归搜索子目录
42
+ const subResult = await getFilesByPattern(fullPath, pattern, recursive);
43
+ return subResult;
44
+ } else if (itemStat.isFile()) {
45
+ // 检查文件名是否匹配正则表达式
46
+ if (pattern.test(fullPath)) {
47
+ return [fullPath];
48
+ }
49
+ }
50
+ } catch (error) {
51
+ console.error(`处理文件/目录时出错: ${fullPath}`, error.message);
52
+ }
53
+
54
+ return [];
55
+ });
56
+
57
+ // 等待所有 Promise 完成
58
+ const results = await Promise.all(promises);
59
+
60
+ // 合并所有结果
61
+ for (const subResult of results) {
62
+ result.push(...subResult);
63
+ }
64
+
65
+ } catch (error) {
66
+ console.error(`读取目录时出错: ${dirPath}`, error.message);
67
+ }
68
+
69
+ return result;
70
+ }
71
+
72
+ export async function listLibs(dirPath) {
73
+ return await getFilesByPattern(dirPath, /d\.ts$/i);
74
+ }
75
+
76
+ export async function listHelperPaths(tsDir, harmonyDir) {
77
+ const etsComponentPath = path.join(harmonyDir, '/ets/component');
78
+ const etsLoaderConfigPath = path.join(harmonyDir, '/ets/build-tools/ets-loader/tsconfig.json');
79
+ const etsLoaderPath = path.join(harmonyDir, '/ets/build-tools/ets-loader');
80
+ const etsLoaderLibs = await listLibs(path.join(etsLoaderPath, '/declarations'));
81
+
82
+ return {
83
+ sdkPath: harmonyDir,
84
+ etsComponentPath,
85
+ etsLoaderConfigPath,
86
+ etsLoaderPath,
87
+ baseUrl: path.join(harmonyDir, '/ets'),
88
+ lib: [...(await listLibs(tsDir)), ...(await listLibs(etsComponentPath)), ...etsLoaderLibs],
89
+ "paths": {
90
+ "*": ["./api/*", "./kits/*", "./arkts/*"],
91
+ "@internal/full/*": ["./api/@internal/full/*"]
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,342 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { getFilesByPattern, listLibs, listHelperPaths } from './lib-expander.js';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+
6
+ // Mock fs module
7
+ vi.mock('fs/promises');
8
+
9
+ // Mock console methods
10
+ const originalConsoleWarn = console.warn;
11
+ const originalConsoleError = console.error;
12
+
13
+ describe('lib-expander', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ console.warn = vi.fn();
17
+ console.error = vi.fn();
18
+ });
19
+
20
+ afterEach(() => {
21
+ console.warn = originalConsoleWarn;
22
+ console.error = originalConsoleError;
23
+ });
24
+
25
+ describe('getFilesByPattern', () => {
26
+ it('should return an empty array when directory does not exist', async () => {
27
+ fs.access.mockRejectedValue(new Error('ENOENT: no such file or directory'));
28
+
29
+ const result = await getFilesByPattern('/nonexistent/path', /\.ts$/);
30
+
31
+ expect(result).toEqual([]);
32
+ expect(console.warn).toHaveBeenCalledWith('路径不存在: /nonexistent/path');
33
+ });
34
+
35
+ it('should return an empty array when path is not a directory', async () => {
36
+ fs.access.mockResolvedValue();
37
+ fs.stat.mockResolvedValue({ isDirectory: () => false });
38
+
39
+ const result = await getFilesByPattern('/some/file.txt', /\.ts$/);
40
+
41
+ expect(result).toEqual([]);
42
+ expect(console.warn).toHaveBeenCalledWith('路径不是目录: /some/file.txt');
43
+ });
44
+
45
+ it('should return matching files from a directory', async () => {
46
+ const mockFiles = ['file1.ts', 'file2.js', 'file3.ts', 'file4.txt'];
47
+ const mockFileStats = {
48
+ isDirectory: () => false,
49
+ isFile: () => true
50
+ };
51
+ const mockDirStats = {
52
+ isDirectory: () => true,
53
+ isFile: () => false
54
+ };
55
+
56
+ fs.access.mockResolvedValue();
57
+ fs.readdir.mockResolvedValue(mockFiles);
58
+
59
+ fs.stat.mockImplementation((filePath) => {
60
+ if (filePath === '/test/dir') {
61
+ return Promise.resolve(mockDirStats);
62
+ }
63
+ if (filePath.includes('file1.ts') || filePath.includes('file2.js') ||
64
+ filePath.includes('file3.ts') || filePath.includes('file4.txt')) {
65
+ return Promise.resolve(mockFileStats);
66
+ }
67
+ return Promise.reject(new Error('File not found'));
68
+ });
69
+
70
+ const pattern = /\.ts$/;
71
+ const result = await getFilesByPattern('/test/dir', pattern);
72
+
73
+ expect(result).toEqual([
74
+ path.join('/test/dir', 'file1.ts'),
75
+ path.join('/test/dir', 'file3.ts')
76
+ ]);
77
+ });
78
+
79
+ it('should search recursively when recursive is true', async () => {
80
+ const mockFiles = ['file1.ts', 'subdir'];
81
+ const mockSubFiles = ['file2.ts', 'file3.js'];
82
+ const mockFileStats = {
83
+ isDirectory: () => false,
84
+ isFile: () => true
85
+ };
86
+ const mockDirStats = {
87
+ isDirectory: () => true,
88
+ isFile: () => false
89
+ };
90
+
91
+ fs.access.mockResolvedValue();
92
+
93
+ fs.readdir.mockImplementation((dirPath) => {
94
+ if (dirPath === '/test/dir') {
95
+ return Promise.resolve(mockFiles);
96
+ } else if (dirPath === '/test/dir/subdir') {
97
+ return Promise.resolve(mockSubFiles);
98
+ }
99
+ return Promise.reject(new Error('Directory not found'));
100
+ });
101
+
102
+ fs.stat.mockImplementation((filePath) => {
103
+ if (filePath === '/test/dir' || filePath === '/test/dir/subdir') {
104
+ return Promise.resolve(mockDirStats);
105
+ }
106
+ if (filePath.includes('file1.ts') || filePath.includes('file2.ts') ||
107
+ filePath.includes('file3.js')) {
108
+ return Promise.resolve(mockFileStats);
109
+ }
110
+ return Promise.reject(new Error('File not found'));
111
+ });
112
+
113
+ const pattern = /\.ts$/;
114
+ const result = await getFilesByPattern('/test/dir', pattern, true);
115
+
116
+ expect(result).toContain(path.join('/test/dir', 'file1.ts'));
117
+ expect(result).toContain(path.join('/test/dir', 'subdir', 'file2.ts'));
118
+ expect(result.length).toBeGreaterThan(0);
119
+ });
120
+
121
+ it('should not search recursively when recursive is false', async () => {
122
+ const mockFiles = ['file1.ts', 'subdir'];
123
+ const mockFileStats = {
124
+ isDirectory: () => false,
125
+ isFile: () => true
126
+ };
127
+ const mockDirStats = {
128
+ isDirectory: () => true,
129
+ isFile: () => false
130
+ };
131
+
132
+ fs.access.mockResolvedValue();
133
+ fs.readdir.mockResolvedValue(mockFiles);
134
+
135
+ fs.stat.mockImplementation((filePath) => {
136
+ if (filePath === '/test/dir') {
137
+ return Promise.resolve(mockDirStats);
138
+ }
139
+ if (filePath.includes('subdir')) {
140
+ return Promise.resolve(mockDirStats);
141
+ }
142
+ if (filePath.includes('file1.ts')) {
143
+ return Promise.resolve(mockFileStats);
144
+ }
145
+ return Promise.reject(new Error('File not found'));
146
+ });
147
+
148
+ const pattern = /\.ts$/;
149
+ const result = await getFilesByPattern('/test/dir', pattern, false);
150
+
151
+ expect(result).toEqual([path.join('/test/dir', 'file1.ts')]);
152
+ });
153
+
154
+ it('should handle file reading errors gracefully', async () => {
155
+ fs.access.mockResolvedValue();
156
+ fs.stat.mockResolvedValue({ isDirectory: () => true });
157
+ fs.readdir.mockResolvedValue(['file1.ts']);
158
+ fs.stat.mockRejectedValue(new Error('Permission denied'));
159
+
160
+ const result = await getFilesByPattern('/test/dir', /\.ts$/);
161
+
162
+ expect(result).toEqual([]);
163
+ expect(console.error).toHaveBeenCalled();
164
+ });
165
+
166
+ it('should handle directory reading errors gracefully', async () => {
167
+ fs.access.mockResolvedValue();
168
+ fs.stat.mockResolvedValue({ isDirectory: () => true });
169
+ fs.readdir.mockRejectedValue(new Error('Permission denied'));
170
+
171
+ const result = await getFilesByPattern('/test/dir', /\.ts$/);
172
+
173
+ expect(result).toEqual([]);
174
+ expect(console.error).toHaveBeenCalledWith('读取目录时出错: /test/dir', 'Permission denied');
175
+ });
176
+
177
+ it('should return empty array when no files match pattern', async () => {
178
+ const mockFiles = ['file1.js', 'file2.txt', 'file3.json'];
179
+ const mockFileStats = {
180
+ isDirectory: () => false,
181
+ isFile: () => true
182
+ };
183
+ const mockDirStats = {
184
+ isDirectory: () => true,
185
+ isFile: () => false
186
+ };
187
+
188
+ fs.access.mockResolvedValue();
189
+ fs.readdir.mockResolvedValue(mockFiles);
190
+
191
+ fs.stat.mockImplementation((filePath) => {
192
+ if (filePath === '/test/dir') {
193
+ return Promise.resolve(mockDirStats);
194
+ }
195
+ return Promise.resolve(mockFileStats);
196
+ });
197
+
198
+ const pattern = /\.ts$/;
199
+ const result = await getFilesByPattern('/test/dir', pattern);
200
+
201
+ expect(result).toEqual([]);
202
+ });
203
+
204
+ it('should handle empty directory', async () => {
205
+ fs.access.mockResolvedValue();
206
+ fs.stat.mockResolvedValue({ isDirectory: () => true });
207
+ fs.readdir.mockResolvedValue([]);
208
+
209
+ const result = await getFilesByPattern('/empty/dir', /\.ts$/);
210
+
211
+ expect(result).toEqual([]);
212
+ });
213
+ });
214
+
215
+ describe('listLibs', () => {
216
+ it('should call getFilesByPattern with d.ts pattern', async () => {
217
+ const mockFiles = ['lib.d.ts', 'file.js', 'test.d.ts', 'node.d.ts'];
218
+ const mockFileStats = {
219
+ isDirectory: () => false,
220
+ isFile: () => true
221
+ };
222
+ const mockDirStats = {
223
+ isDirectory: () => true,
224
+ isFile: () => false
225
+ };
226
+
227
+ fs.access.mockResolvedValue();
228
+ fs.readdir.mockResolvedValue(mockFiles);
229
+
230
+ fs.stat.mockImplementation((filePath) => {
231
+ if (filePath === '/test/path') {
232
+ return Promise.resolve(mockDirStats);
233
+ }
234
+ if (filePath.includes('lib.d.ts') || filePath.includes('file.js') ||
235
+ filePath.includes('test.d.ts') || filePath.includes('node.d.ts')) {
236
+ return Promise.resolve(mockFileStats);
237
+ }
238
+ return Promise.reject(new Error('File not found'));
239
+ });
240
+
241
+ const result = await listLibs('/test/path');
242
+
243
+ expect(result).toEqual([
244
+ path.join('/test/path', 'lib.d.ts'),
245
+ path.join('/test/path', 'test.d.ts'),
246
+ path.join('/test/path', 'node.d.ts')
247
+ ]);
248
+ });
249
+
250
+ it('should be case insensitive for .ts extension', async () => {
251
+ const mockFiles = ['lib.D.TS', 'test.d.Ts'];
252
+ const mockFileStats = {
253
+ isDirectory: () => false,
254
+ isFile: () => true
255
+ };
256
+ const mockDirStats = {
257
+ isDirectory: () => true,
258
+ isFile: () => false
259
+ };
260
+
261
+ fs.access.mockResolvedValue();
262
+ fs.readdir.mockResolvedValue(mockFiles);
263
+
264
+ fs.stat.mockImplementation((filePath) => {
265
+ if (filePath === '/test/path') {
266
+ return Promise.resolve(mockDirStats);
267
+ }
268
+ if (filePath.includes('lib.D.TS') || filePath.includes('test.d.Ts')) {
269
+ return Promise.resolve(mockFileStats);
270
+ }
271
+ return Promise.reject(new Error('File not found'));
272
+ });
273
+
274
+ const result = await listLibs('/test/path');
275
+
276
+ expect(result).toEqual([
277
+ path.join('/test/path', 'lib.D.TS'),
278
+ path.join('/test/path', 'test.d.Ts')
279
+ ]);
280
+ });
281
+
282
+ it('should return empty array when no d.ts files found', async () => {
283
+ const mockFiles = ['file.js', 'test.txt', 'node.json'];
284
+ const mockFileStats = {
285
+ isDirectory: () => false,
286
+ isFile: () => true
287
+ };
288
+ const mockDirStats = {
289
+ isDirectory: () => true,
290
+ isFile: () => false
291
+ };
292
+
293
+ fs.access.mockResolvedValue();
294
+ fs.readdir.mockResolvedValue(mockFiles);
295
+
296
+ fs.stat.mockImplementation((filePath) => {
297
+ if (filePath === '/test/path') {
298
+ return Promise.resolve(mockDirStats);
299
+ }
300
+ return Promise.resolve(mockFileStats);
301
+ });
302
+
303
+ const result = await listLibs('/test/path');
304
+
305
+ expect(result).toEqual([]);
306
+ });
307
+ });
308
+
309
+ describe('listHelperPaths', () => {
310
+ it('should return helper paths structure with correct properties', async () => {
311
+ const harmonyDir = '/harmony/sdk';
312
+ const tsDir = '/ts/lib';
313
+
314
+ // Mock the file system operations
315
+ fs.access.mockResolvedValue();
316
+ fs.readdir.mockResolvedValue([]);
317
+
318
+ const mockDirStats = {
319
+ isDirectory: () => true,
320
+ isFile: () => false
321
+ };
322
+
323
+ fs.stat.mockImplementation((filePath) => {
324
+ return Promise.resolve(mockDirStats);
325
+ });
326
+
327
+ const result = await listHelperPaths(tsDir, harmonyDir);
328
+
329
+ expect(result).toHaveProperty('sdkPath', harmonyDir);
330
+ expect(result).toHaveProperty('etsComponentPath', path.join(harmonyDir, '/ets/component'));
331
+ expect(result).toHaveProperty('etsLoaderConfigPath', path.join(harmonyDir, '/ets/build-tools/ets-loader/tsconfig.json'));
332
+ expect(result).toHaveProperty('etsLoaderPath', path.join(harmonyDir, '/ets/build-tools/ets-loader'));
333
+ expect(result).toHaveProperty('baseUrl', path.join(harmonyDir, '/ets'));
334
+ expect(result).toHaveProperty('lib');
335
+ expect(result).toHaveProperty('paths');
336
+ expect(result.paths).toEqual({
337
+ "*": ["./api/*", "./kits/*", "./arkts/*"],
338
+ "@internal/full/*": ["./api/@internal/full/*"]
339
+ });
340
+ });
341
+ });
342
+ });
package/lib/logger.js ADDED
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ export function createSimpleLogger() {
9
+ // if (process.env.ZED_ETS_LANG_SERVER_LOG !== 'true') {
10
+ // const noop = (_msg) => {};
11
+ // return {
12
+ // info: noop,
13
+ // success: noop,
14
+ // error: console.error,
15
+ // warn: noop,
16
+ // section: noop,
17
+ // data: noop,
18
+ // close: noop,
19
+ // };
20
+ // }
21
+ // Create log file write stream
22
+ const logFilePath = path.join(__dirname, 'arkts-lsw.log');
23
+ const logStream = fs.createWriteStream(logFilePath, { flags: 'w+' });
24
+
25
+ // Function to get current timestamp
26
+ const getTimestamp = () => {
27
+ const now = new Date();
28
+ return now.toISOString().slice(0, 19).replace('T', ' ');
29
+ };
30
+
31
+ // Logging utility
32
+ const logger = {
33
+ info: (msg) => {
34
+ const timestamp = getTimestamp();
35
+ const logMsg = `[${timestamp}] ℹ ${msg}\n`;
36
+ logStream.write(logMsg);
37
+ },
38
+ success: (msg) => {
39
+ const timestamp = getTimestamp();
40
+ const logMsg = `[${timestamp}] ✓ ${msg}\n`;
41
+ logStream.write(logMsg);
42
+ },
43
+ error: (msg) => {
44
+ const timestamp = getTimestamp();
45
+ const logMsg = `[${timestamp}] ✗ ${msg}\n`;
46
+ logStream.write(logMsg);
47
+ process.stderr.write(logMsg);
48
+ },
49
+ warn: (msg) => {
50
+ const timestamp = getTimestamp();
51
+ const logMsg = `[${timestamp}] ⚠ ${msg}\n`;
52
+ logStream.write(logMsg);
53
+ },
54
+ section: (msg) => {
55
+ const timestamp = getTimestamp();
56
+ const logMsg = `\n[${timestamp}] ${msg}\n\n`;
57
+ logStream.write(logMsg);
58
+ },
59
+ data: (label, data) => {
60
+ const timestamp = getTimestamp();
61
+ const logMsg = `[${timestamp}] ${label}: ${JSON.stringify(data, null, 2)}\n`;
62
+ logStream.write(logMsg);
63
+ },
64
+ // Add method to close log stream
65
+ close: () => {
66
+ logStream.end();
67
+ },
68
+ };
69
+
70
+ return logger;
71
+ }
72
+
73
+ export const logger = createSimpleLogger();
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "zed-ets-language-server",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "ETS language server wrapper for Zed ArkTS extension.",
5
+ "type": "module",
5
6
  "main": "index.js",
6
7
  "engines": {
7
- "node": ">=22"
8
+ "node": ">=22.12.0"
8
9
  },
9
10
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
+ "test": "vitest run"
11
12
  },
12
13
  "author": "liuyanghejerry <liuyanghejerry@126.com>",
13
14
  "license": "MIT",
14
15
  "dependencies": {
15
16
  "@arkts/language-server": "^1.2.2"
17
+ },
18
+ "devDependencies": {
19
+ "vitest": "^4.0.9"
16
20
  }
17
21
  }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ globals: true,
7
+ },
8
+ });