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