• 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("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