• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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