/
webServer.ts
359 lines (308 loc) · 17.2 KB
/
webServer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
/* eslint-disable local/boolean-trivia */
namespace ts.projectSystem {
describe("unittests:: tsserver:: webServer", () => {
class TestWorkerSession extends server.WorkerSession {
constructor(host: server.ServerHost, webHost: server.HostWithWriteMessage, options: Partial<server.StartSessionOptions>, logger: server.Logger) {
super(
host,
webHost,
{
globalPlugins: undefined,
pluginProbeLocations: undefined,
allowLocalPluginLoads: undefined,
useSingleInferredProject: true,
useInferredProjectPerProjectRoot: false,
suppressDiagnosticEvents: false,
noGetErrOnBackgroundUpdate: true,
syntaxOnly: undefined,
serverMode: undefined,
...options
},
logger,
server.nullCancellationToken,
() => emptyArray
);
}
getProjectService() {
return this.projectService;
}
}
function setup(logLevel: server.LogLevel | undefined, options?: Partial<server.StartSessionOptions>, importPlugin?: server.ServerHost["importPlugin"]) {
const host = createServerHost([libFile], { windowsStyleRoot: "c:/" });
const messages: any[] = [];
const webHost: server.WebHost = {
readFile: s => host.readFile(s),
fileExists: s => host.fileExists(s),
writeMessage: s => messages.push(s),
};
const webSys = server.createWebSystem(webHost, emptyArray, () => host.getExecutingFilePath());
webSys.importPlugin = importPlugin;
const logger = logLevel !== undefined ? new server.MainProcessLogger(logLevel, webHost) : nullLogger();
const session = new TestWorkerSession(webSys, webHost, { serverMode: LanguageServiceMode.PartialSemantic, ...options }, logger);
return { getMessages: () => messages, clearMessages: () => messages.length = 0, session };
}
describe("open files are added to inferred project and semantic operations succeed", () => {
function verify(logLevel: server.LogLevel | undefined) {
const { session, clearMessages, getMessages } = setup(logLevel);
const service = session.getProjectService();
const file: File = {
path: "^memfs:/sample-folder/large.ts",
content: "export const numberConst = 10; export const arrayConst: Array<string> = [];"
};
session.executeCommand({
seq: 1,
type: "request",
command: protocol.CommandTypes.Open,
arguments: {
file: file.path,
fileContent: file.content
}
});
checkNumberOfProjects(service, { inferredProjects: 1 });
const project = service.inferredProjects[0];
checkProjectActualFiles(project, ["/lib.d.ts", file.path]); // Lib files are rooted
verifyQuickInfo();
verifyGotoDefInLib();
function verifyQuickInfo() {
clearMessages();
const start = protocolFileLocationFromSubstring(file, "numberConst");
session.onMessage({
seq: 2,
type: "request",
command: protocol.CommandTypes.Quickinfo,
arguments: start
});
assert.deepEqual(last(getMessages()), {
seq: 0,
type: "response",
command: protocol.CommandTypes.Quickinfo,
request_seq: 2,
success: true,
performanceData: undefined,
body: {
kind: ScriptElementKind.constElement,
kindModifiers: "export",
start: { line: start.line, offset: start.offset },
end: { line: start.line, offset: start.offset + "numberConst".length },
displayString: "const numberConst: 10",
documentation: "",
tags: []
}
});
verifyLogger();
}
function verifyGotoDefInLib() {
clearMessages();
const start = protocolFileLocationFromSubstring(file, "Array");
session.onMessage({
seq: 3,
type: "request",
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: start
});
assert.deepEqual(last(getMessages()), {
seq: 0,
type: "response",
command: protocol.CommandTypes.DefinitionAndBoundSpan,
request_seq: 3,
success: true,
performanceData: undefined,
body: {
definitions: [{
file: "/lib.d.ts",
...protocolTextSpanWithContextFromSubstring({
fileText: libFile.content,
text: "Array",
contextText: "interface Array<T> { length: number; [n: number]: T; }"
})
}],
textSpan: {
start: { line: start.line, offset: start.offset },
end: { line: start.line, offset: start.offset + "Array".length },
}
}
});
verifyLogger();
}
function verifyLogger() {
const messages = getMessages();
assert.equal(messages.length, logLevel === server.LogLevel.verbose ? 4 : 1, `Expected ${JSON.stringify(messages)}`);
if (logLevel === server.LogLevel.verbose) {
verifyLogMessages(messages[0], "info");
verifyLogMessages(messages[1], "perf");
verifyLogMessages(messages[2], "info");
}
clearMessages();
}
function verifyLogMessages(actual: any, expectedLevel: server.MessageLogLevel) {
assert.equal(actual.type, "log");
assert.equal(actual.level, expectedLevel);
}
}
it("with logging enabled", () => {
verify(server.LogLevel.verbose);
});
it("with logging disabled", () => {
verify(/*logLevel*/ undefined);
});
});
describe("async loaded plugins", () => {
it("plugins are not loaded immediately", async () => {
let pluginModuleInstantiated = false;
let pluginInvoked = false;
const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
await Promise.resolve(); // simulate at least a single turn delay
pluginModuleInstantiated = true;
return {
module: (() => {
pluginInvoked = true;
return { create: info => info.languageService };
}) as server.PluginModuleFactory,
error: undefined
};
};
const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
const projectService = session.getProjectService();
session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
// This should be false because `executeCommand` should have already triggered
// plugin enablement asynchronously and there are no plugin enablements currently
// being processed.
expect(projectService.hasNewPluginEnablementRequests()).eq(false);
// Should be true because async imports have already been triggered in the background
expect(projectService.hasPendingPluginEnablements()).eq(true);
// Should be false because resolution of async imports happens in a later turn.
expect(pluginModuleInstantiated).eq(false);
await projectService.waitForPendingPlugins();
// at this point all plugin modules should have been instantiated and all plugins
// should have been invoked
expect(pluginModuleInstantiated).eq(true);
expect(pluginInvoked).eq(true);
});
it("plugins evaluation in correct order even if imports resolve out of order", async () => {
const pluginADeferred = Utils.defer();
const pluginBDeferred = Utils.defer();
const log: string[] = [];
const importPlugin = async (_root: string, moduleName: string): Promise<server.ModuleImportResult> => {
log.push(`request import ${moduleName}`);
const promise = moduleName === "plugin-a" ? pluginADeferred.promise : pluginBDeferred.promise;
await promise;
log.push(`fulfill import ${moduleName}`);
return {
module: (() => {
log.push(`invoke plugin ${moduleName}`);
return { create: info => info.languageService };
}) as server.PluginModuleFactory,
error: undefined
};
};
const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a", "plugin-b"] }, importPlugin);
const projectService = session.getProjectService();
session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
// wait a turn
await Promise.resolve();
// resolve imports out of order
pluginBDeferred.resolve();
pluginADeferred.resolve();
// wait for load to complete
await projectService.waitForPendingPlugins();
expect(log).to.deep.equal([
"request import plugin-a",
"request import plugin-b",
"fulfill import plugin-b",
"fulfill import plugin-a",
"invoke plugin plugin-a",
"invoke plugin plugin-b",
]);
});
it("sends projectsUpdatedInBackground event", async () => {
const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
await Promise.resolve(); // simulate at least a single turn delay
return {
module: (() => ({ create: info => info.languageService })) as server.PluginModuleFactory,
error: undefined
};
};
const { session, getMessages } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
const projectService = session.getProjectService();
session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
await projectService.waitForPendingPlugins();
expect(getMessages()).to.deep.equal([{
seq: 0,
type: "event",
event: "projectsUpdatedInBackground",
body: {
openFiles: ["^memfs:/foo.ts"]
}
}]);
});
it("adds external files", async () => {
const pluginAShouldLoad = Utils.defer();
const pluginAExternalFilesRequested = Utils.defer();
const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
// wait until the initial external files are requested from the project service.
await pluginAShouldLoad.promise;
return {
module: (() => ({
create: info => info.languageService,
getExternalFiles: () => {
// signal that external files have been requested by the project service.
pluginAExternalFilesRequested.resolve();
return ["external.txt"];
}
})) as server.PluginModuleFactory,
error: undefined
};
};
const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
const projectService = session.getProjectService();
session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
const project = projectService.inferredProjects[0];
// get the external files we know about before plugins are loaded
const initialExternalFiles = project.getExternalFiles();
// we've ready the initial set of external files, allow the plugin to continue loading.
pluginAShouldLoad.resolve();
// wait for plugins
await projectService.waitForPendingPlugins();
// wait for the plugin's external files to be requested
await pluginAExternalFilesRequested.promise;
// get the external files we know aobut after plugins are loaded
const pluginExternalFiles = project.getExternalFiles();
expect(initialExternalFiles).to.deep.equal([]);
expect(pluginExternalFiles).to.deep.equal(["external.txt"]);
});
it("project is closed before plugins are loaded", async () => {
const pluginALoaded = Utils.defer();
const projectClosed = Utils.defer();
const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
// mark that the plugin has started loading
pluginALoaded.resolve();
// wait until after a project close has been requested to continue
await projectClosed.promise;
return {
module: (() => ({ create: info => info.languageService })) as server.PluginModuleFactory,
error: undefined
};
};
const { session, getMessages } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
const projectService = session.getProjectService();
session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
// wait for the plugin to start loading
await pluginALoaded.promise;
// close the project
session.executeCommand({ seq: 2, type: "request", command: protocol.CommandTypes.Close, arguments: { file: "^memfs:/foo.ts" } });
// continue loading the plugin
projectClosed.resolve();
await projectService.waitForPendingPlugins();
// the project was closed before plugins were ready. no project update should have been requested
expect(getMessages()).not.to.deep.equal([{
seq: 0,
type: "event",
event: "projectsUpdatedInBackground",
body: {
openFiles: ["^memfs:/foo.ts"]
}
}]);
});
});
});
}