namespace ts.server { export interface InstallPackageOptionsWithProject extends InstallPackageOptions { projectName: string; projectRootPath: Path; } // for backwards-compatibility // eslint-disable-next-line @typescript-eslint/naming-convention export interface ITypingsInstaller { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProject): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray | undefined): void; attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string | undefined; } export const nullTypingsInstaller: ITypingsInstaller = { isKnownTypesPackageName: returnFalse, // Should never be called because we never provide a types registry. installPackage: notImplemented, enqueueInstallTypingsRequest: noop, attach: noop, onProjectClosed: noop, globalTypingsCacheLocation: undefined! // TODO: GH#18217 }; interface TypingsCacheEntry { readonly typeAcquisition: TypeAcquisition; readonly compilerOptions: CompilerOptions; readonly typings: SortedReadonlyArray; readonly unresolvedImports: SortedReadonlyArray | undefined; /* mainly useful for debugging */ poisoned: boolean; } function setIsEqualTo(arr1: string[] | undefined, arr2: string[] | undefined): boolean { if (arr1 === arr2) { return true; } if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) { return true; } const set = new Map(); let unique = 0; for (const v of arr1!) { if (set.get(v) !== true) { set.set(v, true); unique++; } } for (const v of arr2!) { const isSet = set.get(v); if (isSet === undefined) { return false; } if (isSet === true) { set.set(v, false); unique--; } } return unique === 0; } function typeAcquisitionChanged(opt1: TypeAcquisition, opt2: TypeAcquisition): boolean { return opt1.enable !== opt2.enable || !setIsEqualTo(opt1.include, opt2.include) || !setIsEqualTo(opt1.exclude, opt2.exclude); } function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean { // TODO: add more relevant properties return opt1.allowJs !== opt2.allowJs; } function unresolvedImportsChanged(imports1: SortedReadonlyArray | undefined, imports2: SortedReadonlyArray | undefined): boolean { if (imports1 === imports2) { return false; } return !arrayIsEqualTo(imports1, imports2); } /*@internal*/ export class TypingsCache { private readonly perProjectCache = new Map(); constructor(private readonly installer: ITypingsInstaller) { } isKnownTypesPackageName(name: string): boolean { return this.installer.isKnownTypesPackageName(name); } installPackage(options: InstallPackageOptionsWithProject): Promise { return this.installer.installPackage(options); } enqueueInstallTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray | undefined, forceRefresh: boolean) { const typeAcquisition = project.getTypeAcquisition(); if (!typeAcquisition || !typeAcquisition.enable) { return; } const entry = this.perProjectCache.get(project.getProjectName()); if (forceRefresh || !entry || typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) || compilerOptionsChanged(project.getCompilationSettings(), entry.compilerOptions) || unresolvedImportsChanged(unresolvedImports, entry.unresolvedImports)) { // Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options. // instead it acts as a placeholder to prevent issuing multiple requests this.perProjectCache.set(project.getProjectName(), { compilerOptions: project.getCompilationSettings(), typeAcquisition, typings: entry ? entry.typings : emptyArray, unresolvedImports, poisoned: true }); // something has been changed, issue a request to update typings this.installer.enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports); } } updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, newTypings: string[]) { const typings = sort(newTypings); this.perProjectCache.set(projectName, { compilerOptions, typeAcquisition, typings, unresolvedImports, poisoned: false }); return !typeAcquisition || !typeAcquisition.enable ? emptyArray : typings; } onProjectClosed(project: Project) { this.perProjectCache.delete(project.getProjectName()); this.installer.onProjectClosed(project); } } }