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