• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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