zed-ets-language-server 1.1.1 → 2.0.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/index.js +39 -119
- package/lib/data-parser.js +38 -0
- package/lib/data-parser.test.js +279 -0
- package/lib/lib-expander.js +94 -0
- package/lib/lib-expander.test.js +342 -0
- package/lib/logger.js +73 -0
- package/package.json +6 -3
- package/vitest.config.js +8 -0
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
|
|
6
|
-
import {
|
|
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
|
-
|
|
120
|
-
process.stdin.on('data', (data) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zed-ets-language-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "ETS language server wrapper for Zed ArkTS extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=22"
|
|
8
|
+
"node": ">=22.12.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "vitest run"
|
|
12
12
|
},
|
|
13
13
|
"author": "liuyanghejerry <liuyanghejerry@126.com>",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@arkts/language-server": "^1.2.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"vitest": "^4.0.9"
|
|
17
20
|
}
|
|
18
21
|
}
|