1namespace ts.projectSystem { 2 import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; 3 describe("unittests:: tsserver:: watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => { 4 function verifyCompletionListWithNewFileInSubFolder(scenario: string, tscWatchDirectory: Tsc_WatchDirectory) { 5 it(scenario, () => { 6 const projectFolder = "/a/username/project"; 7 const projectSrcFolder = `${projectFolder}/src`; 8 const configFile: File = { 9 path: `${projectFolder}/tsconfig.json`, 10 content: JSON.stringify({ 11 watchOptions: { 12 synchronousWatchDirectory: true 13 } 14 }) 15 }; 16 const index: File = { 17 path: `${projectSrcFolder}/index.ts`, 18 content: `import {} from "./"` 19 }; 20 const file1: File = { 21 path: `${projectSrcFolder}/file1.ts`, 22 content: "" 23 }; 24 25 const files = [index, file1, configFile, libFile]; 26 const environmentVariables = new Map<string, string>(); 27 environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); 28 const host = createServerHost(files, { environmentVariables }); 29 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 30 openFilesForSession([index], session); 31 session.executeCommandSeq<protocol.CompletionsRequest>({ 32 command: protocol.CommandTypes.CompletionInfo, 33 arguments: protocolFileLocationFromSubstring(index, '"', { index: 1 }) 34 }); 35 36 // Add file2 37 const file2: File = { 38 path: `${projectSrcFolder}/file2.ts`, 39 content: "" 40 }; 41 host.writeFile(file2.path, file2.content); 42 host.runQueuedTimeoutCallbacks(); 43 session.executeCommandSeq<protocol.CompletionsRequest>({ 44 command: protocol.CommandTypes.CompletionInfo, 45 arguments: protocolFileLocationFromSubstring(index, '"', { index: 1 }) 46 }); 47 baselineTsserverLogs("watchEnvironment", scenario, session); 48 }); 49 } 50 51 verifyCompletionListWithNewFileInSubFolder( 52 "uses watchFile when file is added to subfolder", 53 Tsc_WatchDirectory.WatchFile 54 ); 55 verifyCompletionListWithNewFileInSubFolder( 56 "uses non recursive watchDirectory when file is added to subfolder", 57 Tsc_WatchDirectory.NonRecursiveWatchDirectory 58 ); 59 verifyCompletionListWithNewFileInSubFolder( 60 "uses dynamic polling when file is added to subfolder", 61 Tsc_WatchDirectory.DynamicPolling 62 ); 63 }); 64 65 describe("unittests:: tsserver:: watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => { 66 function verifyWatchedDirectories(scenario: string, rootedPath: string, useProjectAtRoot: boolean) { 67 it(scenario, () => { 68 const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`; 69 const configFile: File = { 70 path: root + "project/tsconfig.json", 71 content: "{}" 72 }; 73 const file1: File = { 74 path: root + "project/file1.ts", 75 content: "let x = 10;" 76 }; 77 const file2: File = { 78 path: root + "project/file2.ts", 79 content: "let y = 10;" 80 }; 81 const files = [configFile, file1, file2, libFile]; 82 const host = createServerHost(files, { windowsStyleRoot: "c:/" }); 83 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 84 openFilesForSession([file1], session); 85 baselineTsserverLogs("watchEnvironment", scenario, session); 86 }); 87 } 88 89 verifyWatchedDirectories("files at windows style root", "c:/", /*useProjectAtRoot*/ true); 90 verifyWatchedDirectories("files not at windows style root", "c:/", /*useProjectAtRoot*/ false); 91 verifyWatchedDirectories("files at root", "c:/", /*useProjectAtRoot*/ true); 92 verifyWatchedDirectories("files not at root", "c:/", /*useProjectAtRoot*/ false); 93 }); 94 95 it(`unittests:: tsserver:: watchEnvironment:: tsserverProjectSystem recursive watch directory implementation does not watch files/directories in node_modules starting with "."`, () => { 96 const projectFolder = "/a/username/project"; 97 const projectSrcFolder = `${projectFolder}/src`; 98 const configFile: File = { 99 path: `${projectFolder}/tsconfig.json`, 100 content: "{}" 101 }; 102 const index: File = { 103 path: `${projectSrcFolder}/index.ts`, 104 content: `import {} from "file"` 105 }; 106 const file1: File = { 107 path: `${projectSrcFolder}/file1.ts`, 108 content: "" 109 }; 110 const nodeModulesExistingUnusedFile: File = { 111 path: `${projectFolder}/node_modules/someFile.d.ts`, 112 content: "" 113 }; 114 const environmentVariables = new Map<string, string>(); 115 environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); 116 const host = createServerHost([index, file1, configFile, libFile, nodeModulesExistingUnusedFile], { environmentVariables }); 117 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 118 openFilesForSession([index], session); 119 120 const nodeModulesIgnoredFileFromIgnoreDirectory: File = { 121 path: `${projectFolder}/node_modules/.cache/someFile.d.ts`, 122 content: "" 123 }; 124 125 const nodeModulesIgnoredFile: File = { 126 path: `${projectFolder}/node_modules/.cacheFile.ts`, 127 content: "" 128 }; 129 130 const gitIgnoredFileFromIgnoreDirectory: File = { 131 path: `${projectFolder}/.git/someFile.d.ts`, 132 content: "" 133 }; 134 135 const gitIgnoredFile: File = { 136 path: `${projectFolder}/.gitCache.d.ts`, 137 content: "" 138 }; 139 const emacsIgnoredFileFromIgnoreDirectory: File = { 140 path: `${projectFolder}/src/.#field.ts`, 141 content: "" 142 }; 143 144 [ 145 nodeModulesIgnoredFileFromIgnoreDirectory, 146 nodeModulesIgnoredFile, 147 gitIgnoredFileFromIgnoreDirectory, 148 gitIgnoredFile, 149 emacsIgnoredFileFromIgnoreDirectory 150 ].forEach(ignoredEntity => { 151 host.ensureFileOrFolder(ignoredEntity); 152 host.checkTimeoutQueueLength(0); 153 }); 154 155 baselineTsserverLogs("watchEnvironment", `recursive directory does not watch files starting with dot in node_modules`, session); 156 }); 157 158 it("unittests:: tsserver:: watchEnvironment:: tsserverProjectSystem watching files with network style paths", () => { 159 const logger = createLoggerWithInMemoryLogs(/*host*/ undefined!); // Special handling to ensure same logger is used 160 verifyFilePathStyle("c:/myprojects/project/x.js", logger); 161 verifyFilePathStyle("//vda1cs4850/myprojects/project/x.js", logger); 162 verifyFilePathStyle("//vda1cs4850/c$/myprojects/project/x.js", logger); 163 verifyFilePathStyle("c:/users/username/myprojects/project/x.js", logger); 164 verifyFilePathStyle("//vda1cs4850/c$/users/username/myprojects/project/x.js", logger); 165 baselineTsserverLogs("watchEnvironment", `watching files with network style paths`, { logger }); 166 167 function verifyFilePathStyle(path: string, logger: Logger) { 168 const windowsStyleRoot = path.substring(0, getRootLength(path)); 169 const file: File = { path, content: "const x = 10" }; 170 const host = createServerHost( 171 [libFile, file], 172 { windowsStyleRoot } 173 ); 174 logger.host = host; 175 logger.info(`For files of style ${path}`); 176 const session = createSession(host, { logger }); 177 openFilesForSession([file], session); 178 } 179 }); 180 181 describe("unittests:: tsserver:: watchEnvironment:: handles watch compiler options", () => { 182 it("with watchFile option as host configuration", () => { 183 const configFile: File = { 184 path: "/a/b/tsconfig.json", 185 content: "{}" 186 }; 187 const files = [libFile, commonFile2, configFile]; 188 const host = createServerHost(files.concat(commonFile1)); 189 const logger = createLoggerWithInMemoryLogs(host); 190 const session = createSession(host, { logger }); 191 session.executeCommandSeq<protocol.ConfigureRequest>({ 192 command: protocol.CommandTypes.Configure, 193 arguments: { 194 watchOptions: { 195 watchFile: protocol.WatchFileKind.UseFsEvents 196 } 197 } 198 }); 199 openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); 200 baselineTsserverLogs("watchEnvironment", `with watchFile option as host configuration`, session); 201 }); 202 203 it("with watchDirectory option as host configuration", () => { 204 const configFile: File = { 205 path: "/a/b/tsconfig.json", 206 content: "{}" 207 }; 208 const files = [libFile, commonFile2, configFile]; 209 const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true }); 210 const logger = createLoggerWithInMemoryLogs(host); 211 const session = createSession(host, { logger }); 212 session.executeCommandSeq<protocol.ConfigureRequest>({ 213 command: protocol.CommandTypes.Configure, 214 arguments: { 215 watchOptions: { 216 watchDirectory: protocol.WatchDirectoryKind.UseFsEvents 217 } 218 } 219 }); 220 openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); 221 baselineTsserverLogs("watchEnvironment", `with watchDirectory option as host configuration`, session); 222 }); 223 224 it("with fallbackPolling option as host configuration", () => { 225 const configFile: File = { 226 path: "/a/b/tsconfig.json", 227 content: "{}" 228 }; 229 const files = [libFile, commonFile2, configFile]; 230 const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); 231 const logger = createLoggerWithInMemoryLogs(host); 232 const session = createSession(host, { logger }); 233 session.executeCommandSeq<protocol.ConfigureRequest>({ 234 command: protocol.CommandTypes.Configure, 235 arguments: { 236 watchOptions: { 237 fallbackPolling: protocol.PollingWatchKind.PriorityInterval 238 } 239 } 240 }); 241 openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); 242 baselineTsserverLogs("watchEnvironment", `with fallbackPolling option as host configuration`, session); 243 }); 244 245 it("with watchFile option in configFile", () => { 246 const configFile: File = { 247 path: "/a/b/tsconfig.json", 248 content: JSON.stringify({ 249 watchOptions: { 250 watchFile: "UseFsEvents" 251 } 252 }) 253 }; 254 const files = [libFile, commonFile2, configFile]; 255 const host = createServerHost(files.concat(commonFile1)); 256 const logger = createLoggerWithInMemoryLogs(host); 257 const session = createSession(host, { logger }); 258 openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); 259 baselineTsserverLogs("watchEnvironment", `with watchFile option in configFile`, session); 260 }); 261 262 it("with watchDirectory option in configFile", () => { 263 const configFile: File = { 264 path: "/a/b/tsconfig.json", 265 content: JSON.stringify({ 266 watchOptions: { 267 watchDirectory: "UseFsEvents" 268 } 269 }) 270 }; 271 const files = [libFile, commonFile2, configFile]; 272 const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true }); 273 const logger = createLoggerWithInMemoryLogs(host); 274 const session = createSession(host, { logger }); 275 openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); 276 baselineTsserverLogs("watchEnvironment", `with watchDirectory option in configFile`, session); 277 }); 278 279 it("with fallbackPolling option in configFile", () => { 280 const configFile: File = { 281 path: "/a/b/tsconfig.json", 282 content: JSON.stringify({ 283 watchOptions: { 284 fallbackPolling: "PriorityInterval" 285 } 286 }) 287 }; 288 const files = [libFile, commonFile2, configFile]; 289 const host = createServerHost(files.concat(commonFile1), { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); 290 const logger = createLoggerWithInMemoryLogs(host); 291 const session = createSession(host, { logger }); 292 session.executeCommandSeq<protocol.ConfigureRequest>({ 293 command: protocol.CommandTypes.Configure, 294 arguments: { 295 watchOptions: { 296 fallbackPolling: protocol.PollingWatchKind.PriorityInterval 297 } 298 } 299 }); 300 openFilesForSession([{ file: commonFile1, projectRootPath: "/a/b" }], session); 301 baselineTsserverLogs("watchEnvironment", `with fallbackPolling option in configFile`, session); 302 }); 303 304 describe("excludeDirectories", () => { 305 function setupFiles() { 306 const main: File = { 307 path: `${tscWatch.projectRoot}/src/main.ts`, 308 content: `import { foo } from "bar"; foo();` 309 }; 310 const bar: File = { 311 path: `${tscWatch.projectRoot}/node_modules/bar/index.d.ts`, 312 content: `export { foo } from "./foo";` 313 }; 314 const foo: File = { 315 path: `${tscWatch.projectRoot}/node_modules/bar/foo.d.ts`, 316 content: `export function foo(): string;` 317 }; 318 return { main, bar, foo }; 319 } 320 321 function setupConfigureHost(session: TestSession, configureHost: boolean | undefined) { 322 if (configureHost) { 323 session.executeCommandSeq<protocol.ConfigureRequest>({ 324 command: protocol.CommandTypes.Configure, 325 arguments: { 326 watchOptions: { excludeDirectories: ["node_modules"] } 327 } 328 }); 329 } 330 } 331 function setup(configureHost?: boolean) { 332 const configFile: File = { 333 path: `${tscWatch.projectRoot}/tsconfig.json`, 334 content: JSON.stringify({ include: ["src"], watchOptions: { excludeDirectories: ["node_modules"] } }) 335 }; 336 const { main, bar, foo } = setupFiles(); 337 const files = [libFile, main, bar, foo, configFile]; 338 const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); 339 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 340 setupConfigureHost(session, configureHost); 341 openFilesForSession([main], session); 342 return session; 343 } 344 345 it("with excludeDirectories option in configFile", () => { 346 const session = setup(); 347 baselineTsserverLogs("watchEnvironment", `with excludeDirectories option in configFile`, session); 348 }); 349 350 it("with excludeDirectories option in configuration", () => { 351 const session = setup(/*configureHost*/ true); 352 baselineTsserverLogs("watchEnvironment", `with excludeDirectories option in configuration`, session); 353 }); 354 355 function setupExternalProject(configureHost?: boolean) { 356 const { main, bar, foo } = setupFiles(); 357 const files = [libFile, main, bar, foo]; 358 const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); 359 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 360 setupConfigureHost(session, configureHost); 361 session.executeCommandSeq<protocol.OpenExternalProjectRequest>({ 362 command: protocol.CommandTypes.OpenExternalProject, 363 arguments: { 364 projectFileName: `${tscWatch.projectRoot}/project.csproj`, 365 rootFiles: toExternalFiles([main.path, bar.path, foo.path]), 366 options: { excludeDirectories: ["node_modules"] } 367 } 368 }); 369 openFilesForSession([main], session); 370 return session; 371 } 372 373 it("external project watch options", () => { 374 const session = setupExternalProject(); 375 baselineTsserverLogs("watchEnvironment", `external project watch options`, session); 376 }); 377 378 it("external project watch options in host configuration", () => { 379 const session = setupExternalProject(/*configureHost*/ true); 380 baselineTsserverLogs("watchEnvironment", `external project watch options in host configuration`, session); 381 }); 382 383 it("external project watch options errors", () => { 384 const { main, bar, foo } = setupFiles(); 385 const files = [libFile, main, bar, foo]; 386 const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); 387 const service = createProjectService(host, { logger: createLoggerWithInMemoryLogs(host) }); 388 service.openExternalProject({ 389 projectFileName: `${tscWatch.projectRoot}/project.csproj`, 390 rootFiles: toExternalFiles([main.path, bar.path, foo.path]), 391 options: { excludeDirectories: ["**/../*"] } 392 } as protocol.ExternalProject); 393 service.openClientFile(main.path); 394 const project = service.externalProjects[0]; 395 service.logger.info(JSON.stringify(project.getAllProjectErrors(), undefined, 2)); 396 baselineTsserverLogs("watchEnvironment", `external project watch options errors`, service); 397 }); 398 399 function setupInferredProject(configureHost?: boolean) { 400 const { main, bar, foo } = setupFiles(); 401 const files = [libFile, main, bar, foo]; 402 const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); 403 const session = createSession(host, { useInferredProjectPerProjectRoot: true, logger: createLoggerWithInMemoryLogs(host) }); 404 setupConfigureHost(session, configureHost); 405 session.executeCommandSeq<protocol.SetCompilerOptionsForInferredProjectsRequest>({ 406 command: protocol.CommandTypes.CompilerOptionsForInferredProjects, 407 arguments: { 408 options: { excludeDirectories: ["node_modules"] }, 409 projectRootPath: tscWatch.projectRoot 410 } 411 }); 412 openFilesForSession([{ file: main, projectRootPath: tscWatch.projectRoot }], session); 413 return session; 414 } 415 416 it("inferred project watch options", () => { 417 const session = setupInferredProject(); 418 baselineTsserverLogs("watchEnvironment", `inferred project watch options`, session); 419 }); 420 421 it("inferred project watch options in host configuration", () => { 422 const session = setupInferredProject(/*configureHost*/ true); 423 baselineTsserverLogs("watchEnvironment", `inferred project watch options in host configuration`, session); 424 }); 425 426 it("inferred project watch options errors", () => { 427 const { main, bar, foo } = setupFiles(); 428 const files = [libFile, main, bar, foo]; 429 const host = createServerHost(files, { currentDirectory: tscWatch.projectRoot }); 430 const service = createProjectService(host, { useInferredProjectPerProjectRoot: true, logger: createLoggerWithInMemoryLogs(host) }); 431 service.setCompilerOptionsForInferredProjects({ excludeDirectories: ["**/../*"] }, tscWatch.projectRoot); 432 service.openClientFile(main.path, main.content, ScriptKind.TS, tscWatch.projectRoot); 433 const project = service.inferredProjects[0]; 434 service.logger.info(JSON.stringify(project.getAllProjectErrors(), undefined, 2)); 435 baselineTsserverLogs("watchEnvironment", `inferred project watch options errors`, service); 436 }); 437 }); 438 }); 439 440 describe("unittests:: tsserver:: watchEnvironment:: file names on case insensitive file system", () => { 441 function verifyFileNames(scenario: string, projectRootPath: string) { 442 it(scenario, () => { 443 const file: File = { 444 path: `${projectRootPath}/foo.ts`, 445 content: `import { foo } from "bar"` 446 }; 447 const host = createServerHost([file, libFile]); 448 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 449 openFilesForSession([{ file, projectRootPath }], session); 450 baselineTsserverLogs("watchEnvironment", scenario, session); 451 }); 452 } 453 454 verifyFileNames("project with ascii file names", "/User/userName/Projects/I"); 455 verifyFileNames("project with ascii file names with i", "/User/userName/Projects/i"); 456 verifyFileNames("project with unicode file names", "/User/userName/Projects/İ"); 457 }); 458 459 describe("unittests:: tsserver:: watchEnvironment:: watchFile is single watcher per file", () => { 460 it("when watchFile is single watcher per file", () => { 461 const config: File = { 462 path: `${tscWatch.projectRoot}/tsconfig.json`, 463 content: JSON.stringify({ 464 compilerOptions: { 465 composite: true, 466 resolveJsonModule: true, 467 }, 468 }) 469 }; 470 const index: File = { 471 path: `${tscWatch.projectRoot}/index.ts`, 472 content: `import * as tsconfig from "./tsconfig.json";` 473 }; 474 const host = createServerHost([config, index, libFile]); 475 const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); 476 openFilesForSession([index], session); 477 baselineTsserverLogs("watchEnvironment", "when watchFile is single watcher per file", session); 478 }); 479 }); 480} 481