1namespace ts.projectSystem { 2 const packageJson: File = { 3 path: "/package.json", 4 content: `{ "dependencies": { "mobx": "*" } }` 5 }; 6 const aTs: File = { 7 path: "/src/a.ts", 8 content: "export const foo = 0;", 9 }; 10 const bTs: File = { 11 path: "/src/b.ts", 12 content: "foo", 13 }; 14 const cTs: File = { 15 path: "/src/c.ts", 16 content: "import ", 17 }; 18 const bSymlink: SymLink = { 19 path: "/src/b-link.ts", 20 symLink: "./b.ts", 21 }; 22 const tsconfig: File = { 23 path: "/tsconfig.json", 24 content: `{ "include": ["src"] }`, 25 }; 26 const ambientDeclaration: File = { 27 path: "/src/ambient.d.ts", 28 content: "declare module 'ambient' {}" 29 }; 30 const mobxPackageJson: File = { 31 path: "/node_modules/mobx/package.json", 32 content: `{ "name": "mobx", "version": "1.0.0" }` 33 }; 34 const mobxDts: File = { 35 path: "/node_modules/mobx/index.d.ts", 36 content: "export declare function observable(): unknown;" 37 }; 38 39 describe("unittests:: tsserver:: moduleSpecifierCache", () => { 40 it("caches importability within a file", () => { 41 const { moduleSpecifierCache } = setup(); 42 assert.isFalse(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path, {}, {})?.isBlockedByPackageJsonDependencies); 43 }); 44 45 it("caches module specifiers within a file", () => { 46 const { moduleSpecifierCache, triggerCompletions } = setup(); 47 // Completion at an import statement will calculate and cache module specifiers 48 triggerCompletions({ file: cTs.path, line: 1, offset: cTs.content.length + 1 }); 49 const mobxCache = moduleSpecifierCache.get(cTs.path as Path, mobxDts.path as Path, {}, {}); 50 assert.deepEqual(mobxCache, { 51 modulePaths: [{ 52 path: mobxDts.path, 53 isInNodeModules: true, 54 isRedirect: false 55 }], 56 moduleSpecifiers: ["mobx"], 57 isBlockedByPackageJsonDependencies: false, 58 }); 59 }); 60 61 it("invalidates module specifiers when changes happen in contained node_modules directories", () => { 62 const { host, session, moduleSpecifierCache, triggerCompletions } = setup(host => createLoggerWithInMemoryLogs(host)); 63 // Completion at an import statement will calculate and cache module specifiers 64 triggerCompletions({ file: cTs.path, line: 1, offset: cTs.content.length + 1 }); 65 host.writeFile("/node_modules/.staging/mobx-12345678/package.json", "{}"); 66 host.runQueuedTimeoutCallbacks(); 67 assert.equal(moduleSpecifierCache.count(), 0); 68 baselineTsserverLogs("moduleSpecifierCache", "invalidates module specifiers when changes happen in contained node_modules directories", session); 69 }); 70 71 it("does not invalidate the cache when new files are added", () => { 72 const { host, moduleSpecifierCache } = setup(); 73 host.writeFile("/src/a2.ts", aTs.content); 74 host.runQueuedTimeoutCallbacks(); 75 assert.isFalse(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path, {}, {})?.isBlockedByPackageJsonDependencies); 76 }); 77 78 it("invalidates the cache when symlinks are added or removed", () => { 79 const { host, moduleSpecifierCache } = setup(); 80 host.renameFile(bSymlink.path, "/src/b-link2.ts"); 81 host.runQueuedTimeoutCallbacks(); 82 assert.equal(moduleSpecifierCache.count(), 0); 83 }); 84 85 it("invalidates the cache when local package.json changes", () => { 86 const { host, moduleSpecifierCache } = setup(); 87 host.writeFile("/package.json", `{}`); 88 host.runQueuedTimeoutCallbacks(); 89 assert.equal(moduleSpecifierCache.count(), 0); 90 }); 91 92 it("invalidates the cache when module resolution settings change", () => { 93 const { host, moduleSpecifierCache } = setup(); 94 host.writeFile(tsconfig.path, `{ "compilerOptions": { "moduleResolution": "classic" }, "include": ["src"] }`); 95 host.runQueuedTimeoutCallbacks(); 96 assert.equal(moduleSpecifierCache.count(), 0); 97 }); 98 99 it("invalidates the cache when user preferences change", () => { 100 const { moduleSpecifierCache, session, triggerCompletions } = setup(); 101 const preferences: UserPreferences = { importModuleSpecifierPreference: "project-relative" }; 102 103 assert.ok(getWithPreferences({})); 104 executeSessionRequest<protocol.ConfigureRequest, protocol.ConfigureResponse>(session, protocol.CommandTypes.Configure, { preferences }); 105 // Nothing changes yet 106 assert.ok(getWithPreferences({})); 107 assert.isUndefined(getWithPreferences(preferences)); 108 // Completions will request (getting nothing) and set the cache with new preferences 109 triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); 110 assert.isUndefined(getWithPreferences({})); 111 assert.ok(getWithPreferences(preferences)); 112 113 // Test other affecting preference 114 executeSessionRequest<protocol.ConfigureRequest, protocol.ConfigureResponse>(session, protocol.CommandTypes.Configure, { 115 preferences: { importModuleSpecifierEnding: "js" }, 116 }); 117 triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); 118 assert.isUndefined(getWithPreferences(preferences)); 119 120 function getWithPreferences(preferences: UserPreferences) { 121 return moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path, preferences, {}); 122 } 123 }); 124 }); 125 126 function setup(createLogger?: (host: TestServerHost) => Logger) { 127 const host = createServerHost([aTs, bTs, cTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts]); 128 const session = createSession(host, createLogger && { logger: createLogger(host) }); 129 openFilesForSession([aTs, bTs, cTs], session); 130 const projectService = session.getProjectService(); 131 const project = configuredProjectAt(projectService, 0); 132 executeSessionRequest<protocol.ConfigureRequest, protocol.ConfigureResponse>(session, protocol.CommandTypes.Configure, { 133 preferences: { 134 includeCompletionsForImportStatements: true, 135 includeCompletionsForModuleExports: true, 136 includeCompletionsWithInsertText: true, 137 includeCompletionsWithSnippetText: true, 138 }, 139 }); 140 triggerCompletions({ file: bTs.path, line: 1, offset: 3 }); 141 142 return { host, project, projectService, session, moduleSpecifierCache: project.getModuleSpecifierCache(), triggerCompletions }; 143 144 function triggerCompletions(requestLocation: protocol.FileLocationRequestArgs) { 145 executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, { 146 ...requestLocation, 147 }); 148 } 149 } 150} 151