1namespace ts.tscWatch { 2 describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { 3 const scenario = "resolutionCache"; 4 it("caching works", () => { 5 const root = { 6 path: "/a/d/f0.ts", 7 content: `import {x} from "f1"` 8 }; 9 const imported = { 10 path: "/a/f1.ts", 11 content: `foo()` 12 }; 13 14 const { sys, baseline, oldSnap, cb, getPrograms } = createBaseline(createWatchedSystem([root, imported, libFile])); 15 const host = createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ 16 rootFiles: [root.path], 17 system: sys, 18 options: { module: ModuleKind.AMD }, 19 cb, 20 watchOptions: undefined 21 }); 22 const originalFileExists = host.fileExists; 23 const watch = createWatchProgram(host); 24 let fileExistsIsCalled = false; 25 runWatchBaseline({ 26 scenario: "resolutionCache", 27 subScenario: "caching works", 28 commandLineArgs: ["--w", root.path], 29 sys, 30 baseline, 31 oldSnap, 32 getPrograms, 33 changes: [ 34 { 35 caption: "Adding text doesnt re-resole the imports", 36 change: sys => { 37 // patch fileExists to make sure that disk is not touched 38 host.fileExists = notImplemented; 39 sys.writeFile(root.path, `import {x} from "f1" 40 var x: string = 1;`); 41 }, 42 timeouts: runQueuedTimeoutCallbacks 43 }, 44 { 45 caption: "Resolves f2", 46 change: sys => { 47 host.fileExists = (fileName): boolean => { 48 if (fileName === "lib.d.ts") { 49 return false; 50 } 51 fileExistsIsCalled = true; 52 assert.isTrue(fileName.indexOf("/f2.") !== -1); 53 return originalFileExists.call(host, fileName); 54 }; 55 sys.writeFile(root.path, `import {x} from "f2"`); 56 }, 57 timeouts: sys => { 58 sys.runQueuedTimeoutCallbacks(); 59 assert.isTrue(fileExistsIsCalled); 60 }, 61 }, 62 { 63 caption: "Resolve f1", 64 change: sys => { 65 fileExistsIsCalled = false; 66 host.fileExists = (fileName): boolean => { 67 if (fileName === "lib.d.ts") { 68 return false; 69 } 70 fileExistsIsCalled = true; 71 assert.isTrue(fileName.indexOf("/f1.") !== -1); 72 return originalFileExists.call(host, fileName); 73 }; 74 sys.writeFile(root.path, `import {x} from "f1"`); 75 }, 76 timeouts: sys => { 77 sys.runQueuedTimeoutCallbacks(); 78 assert.isTrue(fileExistsIsCalled); 79 } 80 }, 81 ], 82 watchOrSolution: watch 83 }); 84 }); 85 86 it("loads missing files from disk", () => { 87 const root = { 88 path: `/a/foo.ts`, 89 content: `import {x} from "bar"` 90 }; 91 92 const imported = { 93 path: `/a/bar.d.ts`, 94 content: `export const y = 1;` 95 }; 96 97 const { sys, baseline, oldSnap, cb, getPrograms } = createBaseline(createWatchedSystem([root, libFile])); 98 const host = createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ 99 rootFiles: [root.path], 100 system: sys, 101 options: { module: ModuleKind.AMD }, 102 cb, 103 watchOptions: undefined 104 }); 105 const originalFileExists = host.fileExists; 106 let fileExistsCalledForBar = false; 107 host.fileExists = fileName => { 108 if (fileName === "lib.d.ts") { 109 return false; 110 } 111 if (!fileExistsCalledForBar) { 112 fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; 113 } 114 115 return originalFileExists.call(host, fileName); 116 }; 117 118 const watch = createWatchProgram(host); 119 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); 120 runWatchBaseline({ 121 scenario: "resolutionCache", 122 subScenario: "loads missing files from disk", 123 commandLineArgs: ["--w", root.path], 124 sys, 125 baseline, 126 oldSnap, 127 getPrograms, 128 changes: [{ 129 caption: "write imported file", 130 change: sys => { 131 fileExistsCalledForBar = false; 132 sys.writeFile(root.path,`import {y} from "bar"`); 133 sys.writeFile(imported.path, imported.content); 134 }, 135 timeouts: sys => { 136 sys.runQueuedTimeoutCallbacks(); 137 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); 138 } 139 }], 140 watchOrSolution: watch 141 }); 142 }); 143 144 it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { 145 const root = { 146 path: `/a/foo.ts`, 147 content: `import {x} from "bar"` 148 }; 149 150 const imported = { 151 path: `/a/bar.d.ts`, 152 content: `export const y = 1;export const x = 10;` 153 }; 154 155 const { sys, baseline, oldSnap, cb, getPrograms } = createBaseline(createWatchedSystem([root, imported, libFile])); 156 const host = createWatchCompilerHostOfFilesAndCompilerOptionsForBaseline({ 157 rootFiles: [root.path], 158 system: sys, 159 options: { module: ModuleKind.AMD }, 160 cb, 161 watchOptions: undefined 162 }); 163 const originalFileExists = host.fileExists; 164 let fileExistsCalledForBar = false; 165 host.fileExists = fileName => { 166 if (fileName === "lib.d.ts") { 167 return false; 168 } 169 if (!fileExistsCalledForBar) { 170 fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; 171 } 172 return originalFileExists.call(host, fileName); 173 }; 174 const watch = createWatchProgram(host); 175 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); 176 runWatchBaseline({ 177 scenario: "resolutionCache", 178 subScenario: "should compile correctly when resolved module goes missing and then comes back", 179 commandLineArgs: ["--w", root.path], 180 sys, 181 baseline, 182 oldSnap, 183 getPrograms, 184 changes: [ 185 { 186 caption: "Delete imported file", 187 change: sys => { 188 fileExistsCalledForBar = false; 189 sys.deleteFile(imported.path); 190 }, 191 timeouts: sys => { 192 sys.runQueuedTimeoutCallbacks(); 193 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); 194 }, 195 }, 196 { 197 caption: "Create imported file", 198 change: sys => { 199 fileExistsCalledForBar = false; 200 sys.writeFile(imported.path, imported.content); 201 }, 202 timeouts: sys => { 203 sys.checkTimeoutQueueLengthAndRun(1); // Scheduled invalidation of resolutions 204 sys.checkTimeoutQueueLengthAndRun(1); // Actual update 205 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); 206 }, 207 }, 208 ], 209 watchOrSolution: watch 210 }); 211 }); 212 213 verifyTscWatch({ 214 scenario, 215 subScenario: "works when module resolution changes to ambient module", 216 commandLineArgs: ["-w", "/a/b/foo.ts"], 217 sys: () => createWatchedSystem([{ 218 path: "/a/b/foo.ts", 219 content: `import * as fs from "fs";` 220 }, libFile], { currentDirectory: "/a/b" }), 221 changes: [ 222 { 223 caption: "npm install node types", 224 change: sys => { 225 sys.ensureFileOrFolder({ 226 path: "/a/b/node_modules/@types/node/package.json", 227 content: ` 228{ 229 "main": "" 230} 231` 232 }); 233 sys.ensureFileOrFolder({ 234 path: "/a/b/node_modules/@types/node/index.d.ts", 235 content: ` 236declare module "fs" { 237 export interface Stats { 238 isFile(): boolean; 239 } 240}` 241 }); 242 }, 243 timeouts: runQueuedTimeoutCallbacks, 244 } 245 ] 246 }); 247 248 verifyTscWatch({ 249 scenario, 250 subScenario: "works when included file with ambient module changes", 251 commandLineArgs: ["--w", "/a/b/foo.ts", "/a/b/bar.d.ts"], 252 sys: () => { 253 const root = { 254 path: "/a/b/foo.ts", 255 content: ` 256import * as fs from "fs"; 257import * as u from "url"; 258` 259 }; 260 261 const file = { 262 path: "/a/b/bar.d.ts", 263 content: ` 264declare module "url" { 265 export interface Url { 266 href?: string; 267 } 268} 269` 270 }; 271 return createWatchedSystem([root, file, libFile], { currentDirectory: "/a/b" }); 272 }, 273 changes: [ 274 { 275 caption: "Add fs definition", 276 change: sys => sys.appendFile("/a/b/bar.d.ts", ` 277declare module "fs" { 278 export interface Stats { 279 isFile(): boolean; 280 } 281} 282`), 283 timeouts: runQueuedTimeoutCallbacks, 284 } 285 ] 286 }); 287 288 verifyTscWatch({ 289 scenario, 290 subScenario: "works when reusing program with files from external library", 291 commandLineArgs: ["--w", "-p", "/a/b/projects/myProject/src"], 292 sys: () => { 293 const configDir = "/a/b/projects/myProject/src/"; 294 const file1: File = { 295 path: configDir + "file1.ts", 296 content: 'import module1 = require("module1");\nmodule1("hello");' 297 }; 298 const file2: File = { 299 path: configDir + "file2.ts", 300 content: 'import module11 = require("module1");\nmodule11("hello");' 301 }; 302 const module1: File = { 303 path: "/a/b/projects/myProject/node_modules/module1/index.js", 304 content: "module.exports = options => { return options.toString(); }" 305 }; 306 const configFile: File = { 307 path: configDir + "tsconfig.json", 308 content: JSON.stringify({ 309 compilerOptions: { 310 allowJs: true, 311 rootDir: ".", 312 outDir: "../dist", 313 moduleResolution: "node", 314 maxNodeModuleJsDepth: 1 315 } 316 }) 317 }; 318 return createWatchedSystem([file1, file2, module1, libFile, configFile], { currentDirectory: "/a/b/projects/myProject/" }); 319 }, 320 changes: [ 321 { 322 caption: "Add new line to file1", 323 change: sys => sys.appendFile("/a/b/projects/myProject/src/file1.ts", "\n;"), 324 timeouts: runQueuedTimeoutCallbacks, 325 } 326 ] 327 }); 328 329 verifyTscWatch({ 330 scenario, 331 subScenario: "works when renaming node_modules folder that already contains @types folder", 332 commandLineArgs: ["--w", `${projectRoot}/a.ts`], 333 sys: () => { 334 const file: File = { 335 path: `${projectRoot}/a.ts`, 336 content: `import * as q from "qqq";` 337 }; 338 const module: File = { 339 path: `${projectRoot}/node_modules2/@types/qqq/index.d.ts`, 340 content: "export {}" 341 }; 342 return createWatchedSystem([file, libFile, module], { currentDirectory: projectRoot }); 343 }, 344 changes: [ 345 { 346 caption: "npm install", 347 change: sys => sys.renameFolder(`${projectRoot}/node_modules2`, `${projectRoot}/node_modules`), 348 timeouts: runQueuedTimeoutCallbacks, 349 } 350 ] 351 }); 352 353 describe("ignores files/folder changes in node_modules that start with '.'", () => { 354 function verifyIgnore(subScenario: string, commandLineArgs: readonly string[]) { 355 verifyTscWatch({ 356 scenario, 357 subScenario: `ignores changes in node_modules that start with dot/${subScenario}`, 358 commandLineArgs, 359 sys: () => { 360 const file1: File = { 361 path: `${projectRoot}/test.ts`, 362 content: `import { x } from "somemodule";` 363 }; 364 const file2: File = { 365 path: `${projectRoot}/node_modules/somemodule/index.d.ts`, 366 content: `export const x = 10;` 367 }; 368 const config: File = { 369 path: `${projectRoot}/tsconfig.json`, 370 content: "{}" 371 }; 372 return createWatchedSystem([libFile, file1, file2, config]); 373 }, 374 changes: [ 375 { 376 caption: "npm install file and folder that start with '.'", 377 change: sys => sys.ensureFileOrFolder({ 378 path: `${projectRoot}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, 379 content: JSON.stringify({ something: 10 }) 380 }), 381 timeouts: sys => sys.checkTimeoutQueueLength(0), 382 } 383 ] 384 }); 385 } 386 verifyIgnore("watch without configFile", ["--w", `${projectRoot}/test.ts`]); 387 verifyIgnore("watch with configFile", ["--w", "-p", `${projectRoot}/tsconfig.json`]); 388 }); 389 390 verifyTscWatch({ 391 scenario, 392 subScenario: "when types in compiler option are global and installed at later point", 393 commandLineArgs: ["--w", "-p", `${projectRoot}/tsconfig.json`], 394 sys: () => { 395 const app: File = { 396 path: `${projectRoot}/lib/app.ts`, 397 content: `myapp.component("hello");` 398 }; 399 const tsconfig: File = { 400 path: `${projectRoot}/tsconfig.json`, 401 content: JSON.stringify({ 402 compilerOptions: { 403 module: "none", 404 types: ["@myapp/ts-types"] 405 } 406 }) 407 }; 408 return createWatchedSystem([app, tsconfig, libFile]); 409 }, 410 changes: [ 411 { 412 caption: "npm install ts-types", 413 change: sys => { 414 sys.ensureFileOrFolder({ 415 path: `${projectRoot}/node_modules/@myapp/ts-types/package.json`, 416 content: JSON.stringify({ 417 version: "1.65.1", 418 types: "types/somefile.define.d.ts" 419 }) 420 }); 421 sys.ensureFileOrFolder({ 422 path: `${projectRoot}/node_modules/@myapp/ts-types/types/somefile.define.d.ts`, 423 content: ` 424declare namespace myapp { 425 function component(str: string): number; 426}` 427 }); 428 }, 429 timeouts: sys => { 430 sys.checkTimeoutQueueLengthAndRun(2); // Scheduled invalidation of resolutions, update that gets cancelled and rescheduled by actual invalidation of resolution 431 sys.checkTimeoutQueueLengthAndRun(1); // Actual update 432 }, 433 }, 434 { 435 caption: "No change, just check program", 436 change: noop, 437 timeouts: (sys, [[oldProgram, oldBuilderProgram]], watchorSolution) => { 438 sys.checkTimeoutQueueLength(0); 439 const newProgram = (watchorSolution as WatchOfConfigFile<EmitAndSemanticDiagnosticsBuilderProgram>).getProgram(); 440 assert.strictEqual(newProgram, oldBuilderProgram, "No change so builder program should be same"); 441 assert.strictEqual(newProgram.getProgram(), oldProgram, "No change so program should be same"); 442 } 443 } 444 ] 445 }); 446 447 verifyTscWatch({ 448 scenario, 449 subScenario: "with modules linked to sibling folder", 450 commandLineArgs: ["-w"], 451 sys: () => { 452 const mainPackageRoot = `${projectRoot}/main`; 453 const linkedPackageRoot = `${projectRoot}/linked-package`; 454 const mainFile: File = { 455 path: `${mainPackageRoot}/index.ts`, 456 content: "import { Foo } from '@scoped/linked-package'" 457 }; 458 const config: File = { 459 path: `${mainPackageRoot}/tsconfig.json`, 460 content: JSON.stringify({ 461 compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, 462 files: ["index.ts"] 463 }) 464 }; 465 const linkedPackageInMain: SymLink = { 466 path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, 467 symLink: `${linkedPackageRoot}` 468 }; 469 const linkedPackageJson: File = { 470 path: `${linkedPackageRoot}/package.json`, 471 content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) 472 }; 473 const linkedPackageIndex: File = { 474 path: `${linkedPackageRoot}/dist/index.d.ts`, 475 content: "export * from './other';" 476 }; 477 const linkedPackageOther: File = { 478 path: `${linkedPackageRoot}/dist/other.d.ts`, 479 content: 'export declare const Foo = "BAR";' 480 }; 481 const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; 482 return createWatchedSystem(files, { currentDirectory: mainPackageRoot }); 483 }, 484 changes: emptyArray 485 }); 486 487 describe("works when installing something in node_modules or @types when there is no notification from fs for index file", () => { 488 function getNodeAtTypes() { 489 const nodeAtTypesIndex: File = { 490 path: `${projectRoot}/node_modules/@types/node/index.d.ts`, 491 content: `/// <reference path="base.d.ts" />` 492 }; 493 const nodeAtTypesBase: File = { 494 path: `${projectRoot}/node_modules/@types/node/base.d.ts`, 495 content: `// Base definitions for all NodeJS modules that are not specific to any version of TypeScript: 496/// <reference path="ts3.6/base.d.ts" />` 497 }; 498 const nodeAtTypes36Base: File = { 499 path: `${projectRoot}/node_modules/@types/node/ts3.6/base.d.ts`, 500 content: `/// <reference path="../globals.d.ts" />` 501 }; 502 const nodeAtTypesGlobals: File = { 503 path: `${projectRoot}/node_modules/@types/node/globals.d.ts`, 504 content: `declare var process: NodeJS.Process; 505declare namespace NodeJS { 506 interface Process { 507 on(msg: string): void; 508 } 509}` 510 }; 511 return { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals }; 512 } 513 verifyTscWatch({ 514 scenario, 515 subScenario: "works when installing something in node_modules or @types when there is no notification from fs for index file", 516 commandLineArgs: ["--w", `--extendedDiagnostics`], 517 sys: () => { 518 const file: File = { 519 path: `${projectRoot}/worker.ts`, 520 content: `process.on("uncaughtException");` 521 }; 522 const tsconfig: File = { 523 path: `${projectRoot}/tsconfig.json`, 524 content: "{}" 525 }; 526 const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes(); 527 return createWatchedSystem([file, libFile, tsconfig, nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals], { currentDirectory: projectRoot }); 528 }, 529 changes: [ 530 { 531 caption: "npm ci step one: remove all node_modules files", 532 change: sys => sys.deleteFolder(`${projectRoot}/node_modules/@types`, /*recursive*/ true), 533 timeouts: runQueuedTimeoutCallbacks, 534 }, 535 { 536 caption: `npm ci step two: create atTypes but something else in the @types folder`, 537 change: sys => sys.ensureFileOrFolder({ 538 path: `${projectRoot}/node_modules/@types/mocha/index.d.ts`, 539 content: `export const foo = 10;` 540 }), 541 timeouts: runQueuedTimeoutCallbacks 542 }, 543 { 544 caption: `npm ci step three: create atTypes node folder`, 545 change: sys => sys.ensureFileOrFolder({ path: `${projectRoot}/node_modules/@types/node` }), 546 timeouts: runQueuedTimeoutCallbacks 547 }, 548 { 549 caption: `npm ci step four: create atTypes write all the files but dont invoke watcher for index.d.ts`, 550 change: sys => { 551 const { nodeAtTypesIndex, nodeAtTypesBase, nodeAtTypes36Base, nodeAtTypesGlobals } = getNodeAtTypes(); 552 sys.ensureFileOrFolder(nodeAtTypesBase); 553 sys.ensureFileOrFolder(nodeAtTypesIndex, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); 554 sys.ensureFileOrFolder(nodeAtTypes36Base, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); 555 sys.ensureFileOrFolder(nodeAtTypesGlobals, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); 556 }, 557 timeouts: sys => { 558 sys.runQueuedTimeoutCallbacks(); // update failed lookups 559 sys.runQueuedTimeoutCallbacks(); // actual program update 560 }, 561 }, 562 ] 563 }); 564 }); 565 }); 566} 567