1namespace ts.tscWatch { 2 describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { 3 const scenario = "resolutionCache"; 4 it("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 files = [root, imported, libFile]; 15 const host = createWatchedSystem(files); 16 const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); 17 18 const f1IsNotModule = getDiagnosticOfFileFromProgram(watch.getCurrentProgram().getProgram(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); 19 const cannotFindFoo = getDiagnosticOfFileFromProgram(watch.getCurrentProgram().getProgram(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); 20 21 // ensure that imported file was found 22 checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); 23 24 const originalFileExists = host.fileExists; 25 { 26 const newContent = `import {x} from "f1" 27 var x: string = 1;`; 28 root.content = newContent; 29 host.writeFile(root.path, root.content); 30 31 // patch fileExists to make sure that disk is not touched 32 host.fileExists = notImplemented; 33 34 // trigger synchronization to make sure that import will be fetched from the cache 35 host.runQueuedTimeoutCallbacks(); 36 37 // ensure file has correct number of errors after edit 38 checkOutputErrorsIncremental(host, [ 39 f1IsNotModule, 40 getDiagnosticOfFileFromProgram(watch.getCurrentProgram().getProgram(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, "number", "string"), 41 cannotFindFoo 42 ]); 43 } 44 { 45 let fileExistsIsCalled = false; 46 host.fileExists = (fileName): boolean => { 47 if (fileName === "lib.d.ts") { 48 return false; 49 } 50 fileExistsIsCalled = true; 51 assert.isTrue(fileName.indexOf("/f2.") !== -1); 52 return originalFileExists.call(host, fileName); 53 }; 54 55 root.content = `import {x} from "f2"`; 56 host.writeFile(root.path, root.content); 57 58 // trigger synchronization to make sure that system will try to find 'f2' module on disk 59 host.runQueuedTimeoutCallbacks(); 60 61 // ensure file has correct number of errors after edit 62 checkOutputErrorsIncremental(host, [ 63 getDiagnosticModuleNotFoundOfFile(watch.getCurrentProgram().getProgram(), root, "f2") 64 ]); 65 66 assert.isTrue(fileExistsIsCalled); 67 } 68 { 69 let fileExistsCalled = false; 70 host.fileExists = (fileName): boolean => { 71 if (fileName === "lib.d.ts") { 72 return false; 73 } 74 fileExistsCalled = true; 75 assert.isTrue(fileName.indexOf("/f1.") !== -1); 76 return originalFileExists.call(host, fileName); 77 }; 78 79 const newContent = `import {x} from "f1"`; 80 root.content = newContent; 81 82 host.writeFile(root.path, root.content); 83 host.runQueuedTimeoutCallbacks(); 84 85 checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); 86 assert.isTrue(fileExistsCalled); 87 } 88 }); 89 90 it("loads missing files from disk", () => { 91 const root = { 92 path: `/a/foo.ts`, 93 content: `import {x} from "bar"` 94 }; 95 96 const imported = { 97 path: `/a/bar.d.ts`, 98 content: `export const y = 1;` 99 }; 100 101 const files = [root, libFile]; 102 const host = createWatchedSystem(files); 103 const originalFileExists = host.fileExists; 104 105 let fileExistsCalledForBar = false; 106 host.fileExists = fileName => { 107 if (fileName === "lib.d.ts") { 108 return false; 109 } 110 if (!fileExistsCalledForBar) { 111 fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; 112 } 113 114 return originalFileExists.call(host, fileName); 115 }; 116 117 const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); 118 119 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); 120 checkOutputErrorsInitial(host, [ 121 getDiagnosticModuleNotFoundOfFile(watch.getCurrentProgram().getProgram(), root, "bar") 122 ]); 123 124 fileExistsCalledForBar = false; 125 root.content = `import {y} from "bar"`; 126 host.writeFile(root.path, root.content); 127 host.writeFile(imported.path, imported.content); 128 129 host.runQueuedTimeoutCallbacks(); 130 checkOutputErrorsIncremental(host, emptyArray); 131 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); 132 }); 133 134 it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { 135 const root = { 136 path: `/a/foo.ts`, 137 content: `import {x} from "bar"` 138 }; 139 140 const imported = { 141 path: `/a/bar.d.ts`, 142 content: `export const y = 1;export const x = 10;` 143 }; 144 145 const host = createWatchedSystem([root, libFile, imported]); 146 const originalFileExists = host.fileExists; 147 let fileExistsCalledForBar = false; 148 host.fileExists = fileName => { 149 if (fileName === "lib.d.ts") { 150 return false; 151 } 152 if (!fileExistsCalledForBar) { 153 fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; 154 } 155 return originalFileExists.call(host, fileName); 156 }; 157 158 const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); 159 160 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); 161 checkOutputErrorsInitial(host, emptyArray); 162 163 fileExistsCalledForBar = false; 164 host.deleteFile(imported.path); 165 host.runQueuedTimeoutCallbacks(); 166 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); 167 checkOutputErrorsIncremental(host, [ 168 getDiagnosticModuleNotFoundOfFile(watch.getCurrentProgram().getProgram(), root, "bar") 169 ]); 170 171 fileExistsCalledForBar = false; 172 host.writeFile(imported.path, imported.content); 173 host.checkTimeoutQueueLengthAndRun(1); // Scheduled invalidation of resolutions 174 host.checkTimeoutQueueLengthAndRun(1); // Actual update 175 checkOutputErrorsIncremental(host, emptyArray); 176 assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); 177 }); 178 179 verifyTscWatch({ 180 scenario, 181 subScenario: "works when module resolution changes to ambient module", 182 commandLineArgs: ["-w", "/a/b/foo.ts"], 183 sys: () => createWatchedSystem([{ 184 path: "/a/b/foo.ts", 185 content: `import * as fs from "fs";` 186 }, libFile], { currentDirectory: "/a/b" }), 187 changes: [ 188 { 189 caption: "npm install node types", 190 change: sys => { 191 sys.ensureFileOrFolder({ 192 path: "/a/b/node_modules/@types/node/package.json", 193 content: ` 194{ 195 "main": "" 196} 197` 198 }); 199 sys.ensureFileOrFolder({ 200 path: "/a/b/node_modules/@types/node/index.d.ts", 201 content: ` 202declare module "fs" { 203 export interface Stats { 204 isFile(): boolean; 205 } 206}` 207 }); 208 }, 209 timeouts: runQueuedTimeoutCallbacks, 210 } 211 ] 212 }); 213 214 verifyTscWatch({ 215 scenario, 216 subScenario: "works when included file with ambient module changes", 217 commandLineArgs: ["--w", "/a/b/foo.ts", "/a/b/bar.d.ts"], 218 sys: () => { 219 const root = { 220 path: "/a/b/foo.ts", 221 content: ` 222import * as fs from "fs"; 223import * as u from "url"; 224` 225 }; 226 227 const file = { 228 path: "/a/b/bar.d.ts", 229 content: ` 230declare module "url" { 231 export interface Url { 232 href?: string; 233 } 234} 235` 236 }; 237 return createWatchedSystem([root, file, libFile], { currentDirectory: "/a/b" }); 238 }, 239 changes: [ 240 { 241 caption: "Add fs definition", 242 change: sys => sys.appendFile("/a/b/bar.d.ts", ` 243declare module "fs" { 244 export interface Stats { 245 isFile(): boolean; 246 } 247} 248`), 249 timeouts: runQueuedTimeoutCallbacks, 250 } 251 ] 252 }); 253 254 verifyTscWatch({ 255 scenario, 256 subScenario: "works when reusing program with files from external library", 257 commandLineArgs: ["--w", "-p", "/a/b/projects/myProject/src"], 258 sys: () => { 259 const configDir = "/a/b/projects/myProject/src/"; 260 const file1: File = { 261 path: configDir + "file1.ts", 262 content: 'import module1 = require("module1");\nmodule1("hello");' 263 }; 264 const file2: File = { 265 path: configDir + "file2.ts", 266 content: 'import module11 = require("module1");\nmodule11("hello");' 267 }; 268 const module1: File = { 269 path: "/a/b/projects/myProject/node_modules/module1/index.js", 270 content: "module.exports = options => { return options.toString(); }" 271 }; 272 const configFile: File = { 273 path: configDir + "tsconfig.json", 274 content: JSON.stringify({ 275 compilerOptions: { 276 allowJs: true, 277 rootDir: ".", 278 outDir: "../dist", 279 moduleResolution: "node", 280 maxNodeModuleJsDepth: 1 281 } 282 }) 283 }; 284 return createWatchedSystem([file1, file2, module1, libFile, configFile], { currentDirectory: "/a/b/projects/myProject/" }); 285 }, 286 changes: [ 287 { 288 caption: "Add new line to file1", 289 change: sys => sys.appendFile("/a/b/projects/myProject/src/file1.ts", "\n;"), 290 timeouts: runQueuedTimeoutCallbacks, 291 } 292 ] 293 }); 294 295 verifyTscWatch({ 296 scenario, 297 subScenario: "works when renaming node_modules folder that already contains @types folder", 298 commandLineArgs: ["--w", `${projectRoot}/a.ts`], 299 sys: () => { 300 const file: File = { 301 path: `${projectRoot}/a.ts`, 302 content: `import * as q from "qqq";` 303 }; 304 const module: File = { 305 path: `${projectRoot}/node_modules2/@types/qqq/index.d.ts`, 306 content: "export {}" 307 }; 308 return createWatchedSystem([file, libFile, module], { currentDirectory: projectRoot }); 309 }, 310 changes: [ 311 { 312 caption: "npm install", 313 change: sys => sys.renameFolder(`${projectRoot}/node_modules2`, `${projectRoot}/node_modules`), 314 timeouts: runQueuedTimeoutCallbacks, 315 } 316 ] 317 }); 318 319 describe("ignores files/folder changes in node_modules that start with '.'", () => { 320 function verifyIgnore(subScenario: string, commandLineArgs: readonly string[]) { 321 verifyTscWatch({ 322 scenario, 323 subScenario: `ignores changes in node_modules that start with dot/${subScenario}`, 324 commandLineArgs, 325 sys: () => { 326 const file1: File = { 327 path: `${projectRoot}/test.ts`, 328 content: `import { x } from "somemodule";` 329 }; 330 const file2: File = { 331 path: `${projectRoot}/node_modules/somemodule/index.d.ts`, 332 content: `export const x = 10;` 333 }; 334 const config: File = { 335 path: `${projectRoot}/tsconfig.json`, 336 content: "{}" 337 }; 338 return createWatchedSystem([libFile, file1, file2, config]); 339 }, 340 changes: [ 341 { 342 caption: "npm install file and folder that start with '.'", 343 change: sys => sys.ensureFileOrFolder({ 344 path: `${projectRoot}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, 345 content: JSON.stringify({ something: 10 }) 346 }), 347 timeouts: sys => sys.checkTimeoutQueueLength(0), 348 } 349 ] 350 }); 351 } 352 verifyIgnore("watch without configFile", ["--w", `${projectRoot}/test.ts`]); 353 verifyIgnore("watch with configFile", ["--w", "-p", `${projectRoot}/tsconfig.json`]); 354 }); 355 356 verifyTscWatch({ 357 scenario, 358 subScenario: "when types in compiler option are global and installed at later point", 359 commandLineArgs: ["--w", "-p", `${projectRoot}/tsconfig.json`], 360 sys: () => { 361 const app: File = { 362 path: `${projectRoot}/lib/app.ts`, 363 content: `myapp.component("hello");` 364 }; 365 const tsconfig: File = { 366 path: `${projectRoot}/tsconfig.json`, 367 content: JSON.stringify({ 368 compilerOptions: { 369 module: "none", 370 types: ["@myapp/ts-types"] 371 } 372 }) 373 }; 374 return createWatchedSystem([app, tsconfig, libFile]); 375 }, 376 changes: [ 377 { 378 caption: "npm install ts-types", 379 change: sys => { 380 sys.ensureFileOrFolder({ 381 path: `${projectRoot}/node_modules/@myapp/ts-types/package.json`, 382 content: JSON.stringify({ 383 version: "1.65.1", 384 types: "types/somefile.define.d.ts" 385 }) 386 }); 387 sys.ensureFileOrFolder({ 388 path: `${projectRoot}/node_modules/@myapp/ts-types/types/somefile.define.d.ts`, 389 content: ` 390declare namespace myapp { 391 function component(str: string): number; 392}` 393 }); 394 }, 395 timeouts: sys => { 396 sys.checkTimeoutQueueLengthAndRun(2); // Scheduled invalidation of resolutions, update that gets cancelled and rescheduled by actual invalidation of resolution 397 sys.checkTimeoutQueueLengthAndRun(1); // Actual update 398 }, 399 }, 400 { 401 caption: "No change, just check program", 402 change: noop, 403 timeouts: (sys, [[oldProgram, oldBuilderProgram]], watchorSolution) => { 404 sys.checkTimeoutQueueLength(0); 405 const newProgram = (watchorSolution as Watch).getProgram(); 406 assert.strictEqual(newProgram, oldBuilderProgram, "No change so builder program should be same"); 407 assert.strictEqual(newProgram.getProgram(), oldProgram, "No change so program should be same"); 408 } 409 } 410 ] 411 }); 412 413 verifyTscWatch({ 414 scenario, 415 subScenario: "with modules linked to sibling folder", 416 commandLineArgs: ["-w"], 417 sys: () => { 418 const mainPackageRoot = `${projectRoot}/main`; 419 const linkedPackageRoot = `${projectRoot}/linked-package`; 420 const mainFile: File = { 421 path: `${mainPackageRoot}/index.ts`, 422 content: "import { Foo } from '@scoped/linked-package'" 423 }; 424 const config: File = { 425 path: `${mainPackageRoot}/tsconfig.json`, 426 content: JSON.stringify({ 427 compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, 428 files: ["index.ts"] 429 }) 430 }; 431 const linkedPackageInMain: SymLink = { 432 path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, 433 symLink: `${linkedPackageRoot}` 434 }; 435 const linkedPackageJson: File = { 436 path: `${linkedPackageRoot}/package.json`, 437 content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) 438 }; 439 const linkedPackageIndex: File = { 440 path: `${linkedPackageRoot}/dist/index.d.ts`, 441 content: "export * from './other';" 442 }; 443 const linkedPackageOther: File = { 444 path: `${linkedPackageRoot}/dist/other.d.ts`, 445 content: 'export declare const Foo = "BAR";' 446 }; 447 const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; 448 return createWatchedSystem(files, { currentDirectory: mainPackageRoot }); 449 }, 450 changes: emptyArray 451 }); 452 }); 453} 454