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