1namespace ts.server { 2 export interface InstallPackageOptionsWithProject extends InstallPackageOptions { 3 projectName: string; 4 projectRootPath: Path; 5 } 6 7 // for backwards-compatibility 8 // eslint-disable-next-line @typescript-eslint/naming-convention 9 export interface ITypingsInstaller { 10 isKnownTypesPackageName(name: string): boolean; 11 installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult>; 12 enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string> | undefined): void; 13 attach(projectService: ProjectService): void; 14 onProjectClosed(p: Project): void; 15 readonly globalTypingsCacheLocation: string | undefined; 16 } 17 18 export const nullTypingsInstaller: ITypingsInstaller = { 19 isKnownTypesPackageName: returnFalse, 20 // Should never be called because we never provide a types registry. 21 installPackage: notImplemented, 22 enqueueInstallTypingsRequest: noop, 23 attach: noop, 24 onProjectClosed: noop, 25 globalTypingsCacheLocation: undefined! // TODO: GH#18217 26 }; 27 28 interface TypingsCacheEntry { 29 readonly typeAcquisition: TypeAcquisition; 30 readonly compilerOptions: CompilerOptions; 31 readonly typings: SortedReadonlyArray<string>; 32 readonly unresolvedImports: SortedReadonlyArray<string> | undefined; 33 /* mainly useful for debugging */ 34 poisoned: boolean; 35 } 36 37 function setIsEqualTo(arr1: string[] | undefined, arr2: string[] | undefined): boolean { 38 if (arr1 === arr2) { 39 return true; 40 } 41 if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) { 42 return true; 43 } 44 const set = new Map<string, boolean>(); 45 let unique = 0; 46 47 for (const v of arr1!) { 48 if (set.get(v) !== true) { 49 set.set(v, true); 50 unique++; 51 } 52 } 53 for (const v of arr2!) { 54 const isSet = set.get(v); 55 if (isSet === undefined) { 56 return false; 57 } 58 if (isSet === true) { 59 set.set(v, false); 60 unique--; 61 } 62 } 63 return unique === 0; 64 } 65 66 function typeAcquisitionChanged(opt1: TypeAcquisition, opt2: TypeAcquisition): boolean { 67 return opt1.enable !== opt2.enable || 68 !setIsEqualTo(opt1.include, opt2.include) || 69 !setIsEqualTo(opt1.exclude, opt2.exclude); 70 } 71 72 function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean { 73 // TODO: add more relevant properties 74 return getAllowJSCompilerOption(opt1) !== getAllowJSCompilerOption(opt2); 75 } 76 77 function unresolvedImportsChanged(imports1: SortedReadonlyArray<string> | undefined, imports2: SortedReadonlyArray<string> | undefined): boolean { 78 if (imports1 === imports2) { 79 return false; 80 } 81 return !arrayIsEqualTo(imports1, imports2); 82 } 83 84 /*@internal*/ 85 export class TypingsCache { 86 private readonly perProjectCache = new Map<string, TypingsCacheEntry>(); 87 88 constructor(private readonly installer: ITypingsInstaller) { 89 } 90 91 isKnownTypesPackageName(name: string): boolean { 92 return this.installer.isKnownTypesPackageName(name); 93 } 94 95 installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult> { 96 return this.installer.installPackage(options); 97 } 98 99 enqueueInstallTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray<string> | undefined, forceRefresh: boolean) { 100 const typeAcquisition = project.getTypeAcquisition(); 101 102 if (!typeAcquisition || !typeAcquisition.enable) { 103 return; 104 } 105 106 const entry = this.perProjectCache.get(project.getProjectName()); 107 if (forceRefresh || 108 !entry || 109 typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) || 110 compilerOptionsChanged(project.getCompilationSettings(), entry.compilerOptions) || 111 unresolvedImportsChanged(unresolvedImports, entry.unresolvedImports)) { 112 // Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options. 113 // instead it acts as a placeholder to prevent issuing multiple requests 114 this.perProjectCache.set(project.getProjectName(), { 115 compilerOptions: project.getCompilationSettings(), 116 typeAcquisition, 117 typings: entry ? entry.typings : emptyArray, 118 unresolvedImports, 119 poisoned: true 120 }); 121 // something has been changed, issue a request to update typings 122 this.installer.enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports); 123 } 124 } 125 126 updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>, newTypings: string[]) { 127 const typings = sort(newTypings); 128 this.perProjectCache.set(projectName, { 129 compilerOptions, 130 typeAcquisition, 131 typings, 132 unresolvedImports, 133 poisoned: false 134 }); 135 return !typeAcquisition || !typeAcquisition.enable ? emptyArray : typings; 136 } 137 138 onProjectClosed(project: Project) { 139 this.perProjectCache.delete(project.getProjectName()); 140 this.installer.onProjectClosed(project); 141 } 142 } 143} 144