@teambit/ts-server 0.0.0-b5e3d30a4fe58fedfeafa7448ff4bad8debf4e32

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.
@@ -0,0 +1,373 @@
1
+ /**
2
+ * part of this file was copied over from https://github.com/typescript-language-server/typescript-language-server/blob/master/src/tsp-client.ts
3
+ */
4
+
5
+ /*
6
+ * Copyright (C) 2017, 2018 TypeFox and others.
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as cp from 'child_process';
15
+ import * as readline from 'readline';
16
+ import { Logger } from '@teambit/logger';
17
+ import { Readable, Writable } from 'stream';
18
+ import * as decoder from 'string_decoder';
19
+ import type ts from 'typescript/lib/tsserverlibrary';
20
+ import tempy from 'tempy';
21
+ import { CancellationToken } from 'vscode-jsonrpc';
22
+ import { CommandTypes } from './tsp-command-types';
23
+ import { Deferred } from './utils';
24
+
25
+ export interface TspClientOptions {
26
+ logger: Logger;
27
+ tsserverPath: string;
28
+ logToConsole?: boolean;
29
+ onEvent?: (event: ts.server.protocol.Event) => void;
30
+ }
31
+
32
+ export interface TsServerArgs {
33
+ logFile?: string;
34
+ logVerbosity?: string;
35
+ maxTsServerMemory?: number;
36
+ globalPlugins?: string[];
37
+ pluginProbeLocations?: string[];
38
+ }
39
+
40
+ export interface StandardTsServerRequests {
41
+ [CommandTypes.ApplyCodeActionCommand]: [
42
+ ts.server.protocol.ApplyCodeActionCommandRequestArgs,
43
+ ts.server.protocol.ApplyCodeActionCommandResponse,
44
+ ];
45
+ [CommandTypes.CompletionDetails]: [
46
+ ts.server.protocol.CompletionDetailsRequestArgs,
47
+ ts.server.protocol.CompletionDetailsResponse,
48
+ ];
49
+ [CommandTypes.CompletionInfo]: [ts.server.protocol.CompletionsRequestArgs, ts.server.protocol.CompletionInfoResponse];
50
+ [CommandTypes.Configure]: [ts.server.protocol.ConfigureRequestArguments, ts.server.protocol.ConfigureResponse];
51
+ [CommandTypes.Definition]: [ts.server.protocol.FileLocationRequestArgs, ts.server.protocol.DefinitionResponse];
52
+ [CommandTypes.DefinitionAndBoundSpan]: [
53
+ ts.server.protocol.FileLocationRequestArgs,
54
+ ts.server.protocol.DefinitionInfoAndBoundSpanResponse,
55
+ ];
56
+ [CommandTypes.DocCommentTemplate]: [
57
+ ts.server.protocol.FileLocationRequestArgs,
58
+ ts.server.protocol.DocCommandTemplateResponse,
59
+ ];
60
+ [CommandTypes.DocumentHighlights]: [
61
+ ts.server.protocol.DocumentHighlightsRequestArgs,
62
+ ts.server.protocol.DocumentHighlightsResponse,
63
+ ];
64
+ [CommandTypes.Format]: [ts.server.protocol.FormatRequestArgs, ts.server.protocol.FormatResponse];
65
+ [CommandTypes.Formatonkey]: [ts.server.protocol.FormatOnKeyRequestArgs, ts.server.protocol.FormatResponse];
66
+ [CommandTypes.GetApplicableRefactors]: [
67
+ ts.server.protocol.GetApplicableRefactorsRequestArgs,
68
+ ts.server.protocol.GetApplicableRefactorsResponse,
69
+ ];
70
+ [CommandTypes.GetCodeFixes]: [ts.server.protocol.CodeFixRequestArgs, ts.server.protocol.CodeFixResponse];
71
+ [CommandTypes.GetCombinedCodeFix]: [
72
+ ts.server.protocol.GetCombinedCodeFixRequestArgs,
73
+ ts.server.protocol.GetCombinedCodeFixResponse,
74
+ ];
75
+ [CommandTypes.GetEditsForFileRename]: [
76
+ ts.server.protocol.GetEditsForFileRenameRequestArgs,
77
+ ts.server.protocol.GetEditsForFileRenameResponse,
78
+ ];
79
+ [CommandTypes.GetEditsForRefactor]: [
80
+ ts.server.protocol.GetEditsForRefactorRequestArgs,
81
+ ts.server.protocol.GetEditsForRefactorResponse,
82
+ ];
83
+ [CommandTypes.GetOutliningSpans]: [ts.server.protocol.FileRequestArgs, ts.server.protocol.OutliningSpansResponse];
84
+ [CommandTypes.GetSupportedCodeFixes]: [null, ts.server.protocol.GetSupportedCodeFixesResponse];
85
+ [CommandTypes.Implementation]: [
86
+ ts.server.protocol.FileLocationRequestArgs,
87
+ ts.server.protocol.ImplementationResponse,
88
+ ];
89
+ [CommandTypes.JsxClosingTag]: [ts.server.protocol.JsxClosingTagRequestArgs, ts.server.protocol.JsxClosingTagResponse];
90
+ [CommandTypes.Navto]: [ts.server.protocol.NavtoRequestArgs, ts.server.protocol.NavtoResponse];
91
+ [CommandTypes.NavTree]: [ts.server.protocol.FileRequestArgs, ts.server.protocol.NavTreeResponse];
92
+ [CommandTypes.OrganizeImports]: [
93
+ ts.server.protocol.OrganizeImportsRequestArgs,
94
+ ts.server.protocol.OrganizeImportsResponse,
95
+ ];
96
+ [CommandTypes.PrepareCallHierarchy]: [
97
+ ts.server.protocol.FileLocationRequestArgs,
98
+ ts.server.protocol.PrepareCallHierarchyResponse,
99
+ ];
100
+ [CommandTypes.ProvideCallHierarchyIncomingCalls]: [
101
+ ts.server.protocol.FileLocationRequestArgs,
102
+ ts.server.protocol.ProvideCallHierarchyIncomingCallsResponse,
103
+ ];
104
+ [CommandTypes.ProvideCallHierarchyOutgoingCalls]: [
105
+ ts.server.protocol.FileLocationRequestArgs,
106
+ ts.server.protocol.ProvideCallHierarchyOutgoingCallsResponse,
107
+ ];
108
+ [CommandTypes.ProjectInfo]: [ts.server.protocol.ProjectInfoRequestArgs, ts.server.protocol.ProjectInfoResponse];
109
+ [CommandTypes.ProvideInlayHints]: [ts.server.protocol.InlayHintsRequestArgs, ts.server.protocol.InlayHintsResponse];
110
+ [CommandTypes.Quickinfo]: [ts.server.protocol.FileLocationRequestArgs, ts.server.protocol.QuickInfoResponse];
111
+ [CommandTypes.References]: [ts.server.protocol.FileLocationRequestArgs, ts.server.protocol.ReferencesResponse];
112
+ [CommandTypes.Rename]: [ts.server.protocol.RenameRequestArgs, ts.server.protocol.RenameResponse];
113
+ [CommandTypes.SelectionRange]: [
114
+ ts.server.protocol.SelectionRangeRequestArgs,
115
+ ts.server.protocol.SelectionRangeResponse,
116
+ ];
117
+ [CommandTypes.SignatureHelp]: [ts.server.protocol.SignatureHelpRequestArgs, ts.server.protocol.SignatureHelpResponse];
118
+ [CommandTypes.TypeDefinition]: [
119
+ ts.server.protocol.FileLocationRequestArgs,
120
+ ts.server.protocol.TypeDefinitionResponse,
121
+ ];
122
+ [CommandTypes.UpdateOpen]: [ts.server.protocol.UpdateOpenRequestArgs, ts.server.protocol.Response];
123
+ }
124
+
125
+ export interface NoResponseTsServerRequests {
126
+ [CommandTypes.Change]: [ts.server.protocol.ChangeRequestArgs, null];
127
+ [CommandTypes.Close]: [ts.server.protocol.FileRequestArgs, null];
128
+ [CommandTypes.CompilerOptionsForInferredProjects]: [
129
+ ts.server.protocol.SetCompilerOptionsForInferredProjectsArgs,
130
+ ts.server.protocol.SetCompilerOptionsForInferredProjectsResponse,
131
+ ];
132
+ [CommandTypes.Configure]: [ts.server.protocol.ConfigureRequestArguments, ts.server.protocol.ConfigureResponse];
133
+ [CommandTypes.ConfigurePlugin]: [
134
+ ts.server.protocol.ConfigurePluginRequestArguments,
135
+ ts.server.protocol.ConfigurePluginResponse,
136
+ ];
137
+ [CommandTypes.Open]: [ts.server.protocol.OpenRequestArgs, null];
138
+ }
139
+
140
+ export interface AsyncTsServerRequests {
141
+ [CommandTypes.Geterr]: [ts.server.protocol.GeterrRequestArgs, ts.server.protocol.Response];
142
+ [CommandTypes.GeterrForProject]: [ts.server.protocol.GeterrForProjectRequestArgs, ts.server.protocol.Response];
143
+ }
144
+
145
+ export type TypeScriptRequestTypes = StandardTsServerRequests & NoResponseTsServerRequests & AsyncTsServerRequests;
146
+
147
+ export class ProcessBasedTsServer {
148
+ private readlineInterface: readline.ReadLine | null;
149
+ private tsServerProcess: cp.ChildProcess | null;
150
+ private seq = 0;
151
+
152
+ private readonly deferreds: {
153
+ [seq: number]: Deferred<any>;
154
+ } = {};
155
+
156
+ private logger: Logger;
157
+ private cancellationPipeName: string | undefined;
158
+
159
+ constructor(
160
+ private options: TspClientOptions,
161
+ private tsServerArgs: TsServerArgs = {}
162
+ ) {
163
+ this.logger = options.logger;
164
+ }
165
+
166
+ async restart() {
167
+ this.kill();
168
+ await this.start();
169
+ }
170
+
171
+ start() {
172
+ return new Promise<void>((resolve, reject) => {
173
+ if (this.tsServerProcess) {
174
+ reject(new Error('server already started'));
175
+ return;
176
+ }
177
+
178
+ const { tsserverPath } = this.options;
179
+ const { logFile, logVerbosity, maxTsServerMemory, globalPlugins, pluginProbeLocations } = this.tsServerArgs;
180
+ const args: string[] = [];
181
+ if (logFile) {
182
+ args.push('--logFile', logFile);
183
+ }
184
+ if (logVerbosity) {
185
+ args.push('--logVerbosity', logVerbosity);
186
+ }
187
+ if (globalPlugins && globalPlugins.length) {
188
+ args.push('--globalPlugins', globalPlugins.join(','));
189
+ }
190
+ if (pluginProbeLocations && pluginProbeLocations.length) {
191
+ args.push('--pluginProbeLocations', pluginProbeLocations.join(','));
192
+ }
193
+ this.cancellationPipeName = tempy.file({ name: 'tscancellation' });
194
+ args.push('--cancellationPipeName', `${this.cancellationPipeName}*`);
195
+ this.logger.info(`Starting tsserver : '${tsserverPath} ${args.join(' ')}'`);
196
+ const tsserverPathIsModule = path.extname(tsserverPath) === '.js';
197
+ const options = {
198
+ silent: true,
199
+ execArgv: maxTsServerMemory ? [`--max-old-space-size=${maxTsServerMemory}`] : [],
200
+ };
201
+ this.tsServerProcess = tsserverPathIsModule ? cp.fork(tsserverPath, args, options) : cp.spawn(tsserverPath, args);
202
+
203
+ this.readlineInterface = readline.createInterface(
204
+ this.tsServerProcess.stdout as Readable,
205
+ this.tsServerProcess.stdin as Writable,
206
+ undefined
207
+ );
208
+ process.on('exit', () => {
209
+ this.kill();
210
+ reject(new Error('TSServer was killed'));
211
+ });
212
+
213
+ this.readlineInterface.on('line', (line) => {
214
+ this.processMessage(line, resolve, reject);
215
+ });
216
+
217
+ const dec = new decoder.StringDecoder('utf-8');
218
+ this.tsServerProcess.stderr?.addListener('data', (data) => {
219
+ const stringMsg = typeof data === 'string' ? data : dec.write(data);
220
+ this.logger.error(stringMsg);
221
+ reject(new Error(stringMsg));
222
+ });
223
+ });
224
+ }
225
+
226
+ async notify(command: CommandTypes.Open, args: ts.server.protocol.OpenRequestArgs): Promise<void>;
227
+ async notify(command: CommandTypes.Close, args: ts.server.protocol.FileRequestArgs): Promise<void>;
228
+ async notify(command: CommandTypes.Saveto, args: ts.server.protocol.SavetoRequestArgs): Promise<void>;
229
+ async notify(command: CommandTypes.Change, args: ts.server.protocol.ChangeRequestArgs): Promise<void>;
230
+ async notify(command: string, args: any): Promise<void> {
231
+ await this.ensureServerIsRunning();
232
+ this.sendMessage(command, true, args);
233
+ }
234
+
235
+ async request<K extends keyof TypeScriptRequestTypes>(
236
+ command: K,
237
+ args: TypeScriptRequestTypes[K][0],
238
+ token?: CancellationToken
239
+ ): Promise<TypeScriptRequestTypes[K][1]> {
240
+ await this.ensureServerIsRunning();
241
+ this.sendMessage(command, false, args);
242
+ const seq = this.seq;
243
+ const deferred = new Deferred<TypeScriptRequestTypes[K][1]>();
244
+ this.deferreds[seq] = deferred;
245
+ const request = deferred.promise;
246
+
247
+ let onCancelled;
248
+ if (token) {
249
+ onCancelled = token.onCancellationRequested(() => {
250
+ onCancelled.dispose();
251
+ if (this.cancellationPipeName) {
252
+ const requestCancellationPipeName = `${this.cancellationPipeName}${seq}`;
253
+ fs.writeFile(requestCancellationPipeName, '', (err) => {
254
+ if (!err) {
255
+ // eslint-disable-next-line
256
+ request.then(() =>
257
+ fs.unlink(requestCancellationPipeName, () => {
258
+ /* no-op */
259
+ })
260
+ );
261
+ }
262
+ });
263
+ }
264
+ });
265
+ }
266
+
267
+ try {
268
+ const result = await request;
269
+ onCancelled?.dispose();
270
+ return result;
271
+ } catch (error) {
272
+ this.logger.error(`Error in request: ${error}`);
273
+ throw error;
274
+ }
275
+ }
276
+
277
+ kill() {
278
+ this.tsServerProcess?.kill();
279
+ this.tsServerProcess?.stdin?.destroy();
280
+ this.readlineInterface?.close();
281
+ this.tsServerProcess = null;
282
+ this.readlineInterface = null;
283
+ }
284
+
285
+ private log(msg: string, obj: Record<string, any> = {}) {
286
+ msg = `[tsserver] ${msg}`;
287
+ if (this.options.logToConsole) {
288
+ this.logger.console(`${msg} ${JSON.stringify(obj, undefined, 4)}`);
289
+ } else {
290
+ this.logger.trace(msg, obj);
291
+ }
292
+ }
293
+
294
+ protected sendMessage(command: string, notification: boolean, args?: any): void {
295
+ this.seq += 1;
296
+ const request: ts.server.protocol.Request = {
297
+ command,
298
+ seq: this.seq,
299
+ type: 'request',
300
+ };
301
+ if (args) {
302
+ request.arguments = args;
303
+ }
304
+ const serializedRequest = `${JSON.stringify(request)}\n`;
305
+ this.tsServerProcess?.stdin?.write(serializedRequest);
306
+ this.log(notification ? 'notify' : 'request', request);
307
+ }
308
+
309
+ protected processMessage(untrimmedMessageString: string, resolve?: () => void, reject?: (err) => void): void {
310
+ const messageString = untrimmedMessageString.trim();
311
+ if (!messageString || messageString.startsWith('Content-Length:')) {
312
+ return;
313
+ }
314
+ let message: ts.server.protocol.Message;
315
+
316
+ try {
317
+ message = JSON.parse(messageString);
318
+ } catch {
319
+ // If the message isn't valid JSON, it's not a valid tsserver message. Reject the promise.
320
+ reject?.(new Error(`Received invalid message from TSServer: ${untrimmedMessageString}`));
321
+ return;
322
+ }
323
+
324
+ this.log('processMessage', message);
325
+
326
+ if (this.isEvent(message)) {
327
+ resolve?.();
328
+ }
329
+
330
+ if (this.isResponse(message)) {
331
+ this.resolveResponse(message, message.request_seq, message.success);
332
+ } else if (this.isEvent(message)) {
333
+ if (this.isRequestCompletedEvent(message)) {
334
+ this.resolveResponse(message, message.body.request_seq, true);
335
+ } else if (this.options.onEvent) {
336
+ this.options.onEvent(message);
337
+ }
338
+ }
339
+ }
340
+
341
+ private resolveResponse(message: ts.server.protocol.Message, request_seq: number, success: boolean) {
342
+ const deferred = this.deferreds[request_seq];
343
+ this.log('request completed', { request_seq, success });
344
+ if (deferred) {
345
+ if (success) {
346
+ this.deferreds[request_seq].resolve(message);
347
+ } else {
348
+ this.deferreds[request_seq].reject(message);
349
+ }
350
+ delete this.deferreds[request_seq];
351
+ }
352
+ }
353
+
354
+ private isEvent(message: ts.server.protocol.Message): message is ts.server.protocol.Event {
355
+ return message.type === 'event';
356
+ }
357
+
358
+ private isResponse(message: ts.server.protocol.Message): message is ts.server.protocol.Response {
359
+ return message.type === 'response';
360
+ }
361
+
362
+ private isRequestCompletedEvent(
363
+ message: ts.server.protocol.Message
364
+ ): message is ts.server.protocol.RequestCompletedEvent {
365
+ return this.isEvent(message) && message.event === 'requestCompleted';
366
+ }
367
+
368
+ private async ensureServerIsRunning() {
369
+ if (!this.tsServerProcess) {
370
+ await this.restart();
371
+ }
372
+ }
373
+ }