1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: symLinks", () => { 3 it("rename in common file renames all project", () => { 4 const projects = "/users/username/projects"; 5 const folderA = `${projects}/a`; 6 const aFile: File = { 7 path: `${folderA}/a.ts`, 8 content: `import {C} from "./c/fc"; console.log(C)` 9 }; 10 const aTsconfig: File = { 11 path: `${folderA}/tsconfig.json`, 12 content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) 13 }; 14 const aC: SymLink = { 15 path: `${folderA}/c`, 16 symLink: "../c" 17 }; 18 const aFc = `${folderA}/c/fc.ts`; 19 20 const folderB = `${projects}/b`; 21 const bFile: File = { 22 path: `${folderB}/b.ts`, 23 content: `import {C} from "./c/fc"; console.log(C)` 24 }; 25 const bTsconfig: File = { 26 path: `${folderB}/tsconfig.json`, 27 content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) 28 }; 29 const bC: SymLink = { 30 path: `${folderB}/c`, 31 symLink: "../c" 32 }; 33 const bFc = `${folderB}/c/fc.ts`; 34 35 const folderC = `${projects}/c`; 36 const cFile: File = { 37 path: `${folderC}/fc.ts`, 38 content: `export const C = 8` 39 }; 40 41 const files = [cFile, libFile, aFile, aTsconfig, aC, bFile, bTsconfig, bC]; 42 const host = createServerHost(files); 43 const session = createSession(host); 44 const projectService = session.getProjectService(); 45 openFilesForSession( 46 [ 47 { file: aFile, projectRootPath: folderA }, 48 { file: bFile, projectRootPath: folderB }, 49 { file: aFc, projectRootPath: folderA }, 50 { file: bFc, projectRootPath: folderB }, 51 ], 52 session); 53 checkNumberOfProjects(projectService, { configuredProjects: 2 }); 54 assert.isDefined(projectService.configuredProjects.get(aTsconfig.path)); 55 assert.isDefined(projectService.configuredProjects.get(bTsconfig.path)); 56 57 const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, { file: aFc, ...protocolLocationFromSubstring(cFile.content, "C") }); 58 59 assert.equal(aFile.content, bFile.content); 60 const abLocs: protocol.RenameTextSpan[] = [ 61 protocolRenameSpanFromSubstring({ 62 fileText: aFile.content, 63 text: "C", 64 contextText: `import {C} from "./c/fc";` 65 }), 66 protocolRenameSpanFromSubstring({ 67 fileText: aFile.content, 68 text: "C", 69 options: { index: 1 } 70 }), 71 ]; 72 const span = protocolRenameSpanFromSubstring({ 73 fileText: cFile.content, 74 text: "C", 75 contextText: "export const C = 8" 76 }); 77 const cLocs: protocol.RenameTextSpan[] = [span]; 78 assert.deepEqual<protocol.RenameResponseBody | undefined>(response, { 79 info: { 80 canRename: true, 81 displayName: "C", 82 fileToRename: undefined, 83 fullDisplayName: '"/users/username/projects/a/c/fc".C', 84 kind: ScriptElementKind.constElement, 85 kindModifiers: ScriptElementKindModifier.exportedModifier, 86 triggerSpan: protocolTextSpanFromSubstring(cFile.content, "C"), 87 }, 88 locs: [ 89 { file: aFc, locs: cLocs }, 90 { file: aFile.path, locs: abLocs }, 91 { file: bFc, locs: cLocs }, 92 { file: bFile.path, locs: abLocs }, 93 ], 94 }); 95 }); 96 97 describe("module resolution when symlinked folder contents change and resolve modules", () => { 98 const projectRootPath = "/users/username/projects/myproject"; 99 const packages = `${projectRootPath}/javascript/packages`; 100 const recognizersDateTime = `${packages}/recognizers-date-time`; 101 const recognizersText = `${packages}/recognizers-text`; 102 const recognizersTextDist = `${recognizersText}/dist`; 103 const moduleName = "@microsoft/recognizers-text"; 104 const moduleNameInFile = `"${moduleName}"`; 105 const recognizersDateTimeSrcFile: File = { 106 path: `${recognizersDateTime}/src/datetime/baseDate.ts`, 107 content: `import {C} from ${moduleNameInFile}; 108new C();` 109 }; 110 const recognizerDateTimeTsconfigPath = `${recognizersDateTime}/tsconfig.json`; 111 const recognizerDateTimeTsconfigWithoutPathMapping: File = { 112 path: recognizerDateTimeTsconfigPath, 113 content: JSON.stringify({ 114 include: ["src"] 115 }) 116 }; 117 const recognizerDateTimeTsconfigWithPathMapping: File = { 118 path: recognizerDateTimeTsconfigPath, 119 content: JSON.stringify({ 120 compilerOptions: { 121 rootDir: "src", 122 baseUrl: "./", 123 paths: { 124 "@microsoft/*": ["../*"] 125 } 126 }, 127 include: ["src"] 128 }) 129 }; 130 const nodeModulesRecorgnizersText: SymLink = { 131 path: `${recognizersDateTime}/node_modules/@microsoft/recognizers-text`, 132 symLink: recognizersText 133 }; 134 const recognizerTextSrcFile: File = { 135 path: `${recognizersText}/src/recognizers-text.ts`, 136 content: `export class C { method () { return 10; } }` 137 }; 138 const recongnizerTextDistTypingFile: File = { 139 path: `${recognizersTextDist}/types/recognizers-text.d.ts`, 140 content: `export class C { method(): number; }` 141 }; 142 const recongnizerTextPackageJson: File = { 143 path: `${recognizersText}/package.json`, 144 content: JSON.stringify({ 145 typings: "dist/types/recognizers-text.d.ts" 146 }) 147 }; 148 const filesInProjectWithUnresolvedModule = [recognizerDateTimeTsconfigPath, libFile.path, recognizersDateTimeSrcFile.path]; 149 const filesInProjectWithResolvedModule = [...filesInProjectWithUnresolvedModule, recongnizerTextDistTypingFile.path]; 150 151 function verifyErrors(session: TestSession, semanticErrors: protocol.Diagnostic[]) { 152 verifyGetErrRequest({ 153 session, 154 host: session.testhost, 155 expected: [{ 156 file: recognizersDateTimeSrcFile, 157 syntax: [], 158 semantic: semanticErrors, 159 suggestion: [] 160 }] 161 }); 162 } 163 164 function verifyWatchedFilesAndDirectories(host: TestServerHost, files: string[], recursiveDirectories: ReadonlyESMap<string, number>, nonRecursiveDirectories: string[]) { 165 checkWatchedFilesDetailed(host, files.filter(f => f !== recognizersDateTimeSrcFile.path), 1); 166 checkWatchedDirectoriesDetailed(host, nonRecursiveDirectories, 1, /*recursive*/ false); 167 checkWatchedDirectoriesDetailed(host, recursiveDirectories, /*recursive*/ true); 168 } 169 170 function createSessionAndOpenFile(host: TestServerHost) { 171 const session = createSession(host, { canUseEvents: true }); 172 session.executeCommandSeq<protocol.OpenRequest>({ 173 command: protocol.CommandTypes.Open, 174 arguments: { 175 file: recognizersDateTimeSrcFile.path, 176 projectRootPath 177 } 178 }); 179 return session; 180 } 181 182 function verifyModuleResolution(withPathMapping: boolean) { 183 describe(withPathMapping ? "when tsconfig file contains path mapping" : "when tsconfig does not contain path mapping", () => { 184 const filesWithSources = [libFile, recognizersDateTimeSrcFile, withPathMapping ? recognizerDateTimeTsconfigWithPathMapping : recognizerDateTimeTsconfigWithoutPathMapping, recognizerTextSrcFile, recongnizerTextPackageJson]; 185 186 const watchedDirectoriesWithResolvedModule = arrayToMap(getTypeRootsFromLocation(recognizersDateTime), k => k, () => 1); 187 watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/src`, withPathMapping ? 1 : 2); // wild card + failed lookups 188 if (!withPathMapping) { 189 watchedDirectoriesWithResolvedModule.set(`${recognizersDateTime}/node_modules`, 1); // failed lookups 190 } 191 const watchedDirectoriesWithUnresolvedModule = new Map(watchedDirectoriesWithResolvedModule); 192 watchedDirectoriesWithUnresolvedModule.set(`${recognizersDateTime}/src`, 2); // wild card + failed lookups 193 [`${recognizersDateTime}/node_modules`, ...(withPathMapping ? [recognizersText] : emptyArray), ...getNodeModuleDirectories(packages)].forEach(d => { 194 watchedDirectoriesWithUnresolvedModule.set(d, 1); 195 }); 196 const nonRecursiveWatchedDirectories = withPathMapping ? [packages] : emptyArray; 197 198 function verifyProjectWithResolvedModule(session: TestSession) { 199 const projectService = session.getProjectService(); 200 const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!; 201 checkProjectActualFiles(project, filesInProjectWithResolvedModule); 202 verifyWatchedFilesAndDirectories(session.testhost, filesInProjectWithResolvedModule, watchedDirectoriesWithResolvedModule, nonRecursiveWatchedDirectories); 203 verifyErrors(session, []); 204 } 205 206 function verifyProjectWithUnresolvedModule(session: TestSession) { 207 const projectService = session.getProjectService(); 208 const project = projectService.configuredProjects.get(recognizerDateTimeTsconfigPath)!; 209 checkProjectActualFiles(project, filesInProjectWithUnresolvedModule); 210 verifyWatchedFilesAndDirectories(session.testhost, filesInProjectWithUnresolvedModule, watchedDirectoriesWithUnresolvedModule, nonRecursiveWatchedDirectories); 211 const startOffset = recognizersDateTimeSrcFile.content.indexOf('"') + 1; 212 verifyErrors(session, [ 213 createDiagnostic({ line: 1, offset: startOffset }, { line: 1, offset: startOffset + moduleNameInFile.length }, Diagnostics.Cannot_find_module_0_or_its_corresponding_type_declarations, [moduleName]) 214 ]); 215 } 216 217 it("when project compiles from sources", () => { 218 const host = createServerHost(filesWithSources); 219 const session = createSessionAndOpenFile(host); 220 verifyProjectWithUnresolvedModule(session); 221 222 host.ensureFileOrFolder(nodeModulesRecorgnizersText); 223 host.writeFile(recongnizerTextDistTypingFile.path, recongnizerTextDistTypingFile.content); 224 host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions 225 host.runQueuedTimeoutCallbacks(); // Actual update 226 227 verifyProjectWithResolvedModule(session); 228 }); 229 230 it("when project has node_modules setup but doesnt have modules in typings folder and then recompiles", () => { 231 const host = createServerHost([...filesWithSources, nodeModulesRecorgnizersText]); 232 const session = createSessionAndOpenFile(host); 233 verifyProjectWithUnresolvedModule(session); 234 235 host.writeFile(recongnizerTextDistTypingFile.path, recongnizerTextDistTypingFile.content); 236 host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions 237 host.runQueuedTimeoutCallbacks(); // Actual update 238 239 if (withPathMapping) { 240 verifyProjectWithResolvedModule(session); 241 } 242 else { 243 // Cannot handle the resolution update 244 verifyProjectWithUnresolvedModule(session); 245 } 246 }); 247 248 it("when project recompiles after deleting generated folders", () => { 249 const host = createServerHost([...filesWithSources, nodeModulesRecorgnizersText, recongnizerTextDistTypingFile]); 250 const session = createSessionAndOpenFile(host); 251 252 verifyProjectWithResolvedModule(session); 253 254 host.deleteFolder(recognizersTextDist, /*recursive*/ true); 255 host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions 256 host.runQueuedTimeoutCallbacks(); // Actual update 257 258 verifyProjectWithUnresolvedModule(session); 259 260 host.ensureFileOrFolder(recongnizerTextDistTypingFile); 261 host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions 262 host.runQueuedTimeoutCallbacks(); // Actual update 263 264 if (withPathMapping) { 265 verifyProjectWithResolvedModule(session); 266 } 267 else { 268 // Cannot handle the resolution update 269 verifyProjectWithUnresolvedModule(session); 270 } 271 }); 272 }); 273 } 274 275 verifyModuleResolution(/*withPathMapping*/ false); 276 verifyModuleResolution(/*withPathMapping*/ true); 277 }); 278 }); 279} 280