Typings cache need not be a map but directly on the project (#59043)

This commit is contained in:
Sheetal Nandi
2024-06-26 12:47:06 -07:00
committed by GitHub
parent a6bc4ecbe5
commit 3b4cfb5e9b
6 changed files with 162 additions and 198 deletions
-1
View File
@@ -8,7 +8,6 @@ export * from "../utilities.js";
import * as protocol from "./ts.server.protocol.js";
export { protocol };
export * from "../scriptInfo.js";
export * from "../typingsCache.js";
export * from "../project.js";
export * from "../editorServices.js";
export * from "../moduleSpecifierCache.js";
+18 -9
View File
@@ -83,6 +83,7 @@ import {
noop,
normalizePath,
normalizeSlashes,
notImplemented,
optionDeclarations,
optionsForWatch,
orderedRemoveItem,
@@ -170,7 +171,6 @@ import {
Msg,
NormalizedPath,
normalizedPathToPath,
nullTypingsInstaller,
PackageInstalledResponse,
PackageJsonCache,
Project,
@@ -183,7 +183,6 @@ import {
SetTypings,
ThrottledOperations,
toNormalizedPath,
TypingsCache,
WatchTypingLocations,
} from "./_namespaces/ts.server.js";
import * as protocol from "./protocol.js";
@@ -569,6 +568,16 @@ function findProjectByName<T extends Project>(projectName: string, projects: T[]
}
}
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
};
const noopConfigFileWatcher: FileWatcher = { close: noop };
/** @internal */
@@ -1173,9 +1182,6 @@ function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseW
}
export class ProjectService {
/** @internal */
readonly typingsCache: TypingsCache;
/** @internal */
readonly documentRegistry: DocumentRegistry;
@@ -1367,8 +1373,6 @@ export class ProjectService {
this.typingsInstaller.attach(this);
this.typingsCache = new TypingsCache(this.typingsInstaller);
this.hostConfiguration = {
formatCodeOptions: getDefaultFormatCodeSettings(this.host.newLine),
preferences: emptyOptions,
@@ -1481,11 +1485,16 @@ export class ProjectService {
switch (response.kind) {
case ActionSet:
// Update the typing files and update the project
project.updateTypingFiles(this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typeAcquisition, response.unresolvedImports, response.typings));
project.updateTypingFiles(
response.compilerOptions,
response.typeAcquisition,
response.unresolvedImports,
response.typings,
);
return;
case ActionInvalidate:
// Do not clear resolution cache, there was changes detected in typings, so enque typing request and let it get us correct results
this.typingsCache.enqueueInstallTypingsForProject(project, project.lastCachedUnresolvedImportsList, /*forceRefresh*/ true);
project.enqueueInstallTypingsForProject(/*forceRefresh*/ true);
return;
}
}
+105 -10
View File
@@ -4,6 +4,7 @@ import {
append,
ApplyCodeActionCommandResult,
arrayFrom,
arrayIsEqualTo,
arrayToMap,
BuilderState,
CachedDirectoryStructureHost,
@@ -143,6 +144,7 @@ import {
ModuleImportResult,
Msg,
NormalizedPath,
nullTypingsInstaller,
PackageJsonWatcher,
ProjectOptions,
ProjectService,
@@ -150,7 +152,6 @@ import {
ServerHost,
Session,
toNormalizedPath,
TypingsCache,
updateProjectIfDirty,
} from "./_namespaces/ts.server.js";
import * as protocol from "./protocol.js";
@@ -306,6 +307,62 @@ const enum TypingWatcherType {
type TypingWatchers = Map<Path, FileWatcher> & { isInvoked?: boolean; };
interface TypingsCacheEntry {
readonly typeAcquisition: TypeAcquisition;
readonly compilerOptions: CompilerOptions;
readonly typings: SortedReadonlyArray<string>;
readonly unresolvedImports: SortedReadonlyArray<string> | 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<string, boolean>();
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 getAllowJSCompilerOption(opt1) !== getAllowJSCompilerOption(opt2);
}
function unresolvedImportsChanged(imports1: SortedReadonlyArray<string> | undefined, imports2: SortedReadonlyArray<string> | undefined): boolean {
if (imports1 === imports2) {
return false;
}
return !arrayIsEqualTo(imports1, imports2);
}
export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
private rootFilesMap = new Map<Path, ProjectRootFile>();
private program: Program | undefined;
@@ -388,6 +445,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
/** @internal */
typingFiles: SortedReadonlyArray<string> = emptyArray;
private typingsCache: TypingsCacheEntry | undefined;
private typingWatchers: TypingWatchers | undefined;
/** @internal */
@@ -578,10 +637,10 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
isKnownTypesPackageName(name: string): boolean {
return this.typingsCache.isKnownTypesPackageName(name);
return this.projectService.typingsInstaller.isKnownTypesPackageName(name);
}
installPackage(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult> {
return this.typingsCache.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) });
return this.projectService.typingsInstaller.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) });
}
/** @internal */
@@ -589,10 +648,6 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
return this.getGlobalCache();
}
private get typingsCache(): TypingsCache {
return this.projectService.typingsCache;
}
/** @internal */
getSymlinkCache(): SymlinkCache {
if (!this.symlinks) {
@@ -1059,7 +1114,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
close() {
this.projectService.typingsCache.onProjectClosed(this);
if (this.typingsCache) this.projectService.typingsInstaller.onProjectClosed(this);
this.typingsCache = undefined;
this.closeWatchingTypingLocations();
// if we have a program - release all files that are enlisted in program but arent root
// The releasing of the roots happens later
@@ -1379,7 +1435,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
this.lastCachedUnresolvedImportsList = getUnresolvedImports(this.program!, this.cachedUnresolvedImportsPerFile);
}
this.projectService.typingsCache.enqueueInstallTypingsForProject(this, this.lastCachedUnresolvedImportsList, hasAddedorRemovedFiles);
this.enqueueInstallTypingsForProject(hasAddedorRemovedFiles);
}
else {
this.lastCachedUnresolvedImportsList = undefined;
@@ -1401,7 +1457,46 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
/** @internal */
updateTypingFiles(typingFiles: SortedReadonlyArray<string>) {
enqueueInstallTypingsForProject(forceRefresh: boolean) {
const typeAcquisition = this.getTypeAcquisition();
if (!typeAcquisition || !typeAcquisition.enable || this.projectService.typingsInstaller === nullTypingsInstaller) {
return;
}
const entry = this.typingsCache;
if (
forceRefresh ||
!entry ||
typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) ||
compilerOptionsChanged(this.getCompilationSettings(), entry.compilerOptions) ||
unresolvedImportsChanged(this.lastCachedUnresolvedImportsList, 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.typingsCache = {
compilerOptions: this.getCompilationSettings(),
typeAcquisition,
typings: entry ? entry.typings : emptyArray,
unresolvedImports: this.lastCachedUnresolvedImportsList,
poisoned: true,
};
// something has been changed, issue a request to update typings
this.projectService.typingsInstaller.enqueueInstallTypingsRequest(this, typeAcquisition, this.lastCachedUnresolvedImportsList);
}
}
/** @internal */
updateTypingFiles(compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>, newTypings: string[]) {
const typings = sort(newTypings);
this.typingsCache = {
compilerOptions,
typeAcquisition,
typings,
unresolvedImports,
poisoned: false,
};
const typingFiles = !typeAcquisition || !typeAcquisition.enable ? emptyArray : typings;
if (enumerateInsertsAndDeletes<string, string>(typingFiles, this.typingFiles, getStringComparer(!this.useCaseSensitiveFileNames()), /*inserted*/ noop, removed => this.detachScriptInfoFromProject(removed))) {
// If typing files changed, then only schedule project update
this.typingFiles = typingFiles;
+25
View File
@@ -1,10 +1,19 @@
import {
ApplyCodeActionCommandResult,
DirectoryWatcherCallback,
FileWatcher,
FileWatcherCallback,
InstallPackageOptions,
Path,
SortedReadonlyArray,
System,
TypeAcquisition,
WatchOptions,
} from "./_namespaces/ts.js";
import {
Project,
ProjectService,
} from "./_namespaces/ts.server.js";
export interface CompressedData {
length: number;
@@ -30,3 +39,19 @@ export interface ServerHost extends System {
/** @internal */
importPlugin?(root: string, moduleName: string): Promise<ModuleImportResult>;
}
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<ApplyCodeActionCommandResult>;
enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string> | undefined): void;
attach(projectService: ProjectService): void;
onProjectClosed(p: Project): void;
readonly globalTypingsCacheLocation: string | undefined;
}
-164
View File
@@ -1,164 +0,0 @@
import {
ApplyCodeActionCommandResult,
arrayIsEqualTo,
CompilerOptions,
getAllowJSCompilerOption,
InstallPackageOptions,
noop,
notImplemented,
Path,
returnFalse,
sort,
SortedReadonlyArray,
TypeAcquisition,
} from "./_namespaces/ts.js";
import {
emptyArray,
Project,
ProjectService,
} from "./_namespaces/ts.server.js";
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<ApplyCodeActionCommandResult>;
enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string> | 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<string>;
readonly unresolvedImports: SortedReadonlyArray<string> | 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<string, boolean>();
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 getAllowJSCompilerOption(opt1) !== getAllowJSCompilerOption(opt2);
}
function unresolvedImportsChanged(imports1: SortedReadonlyArray<string> | undefined, imports2: SortedReadonlyArray<string> | undefined): boolean {
if (imports1 === imports2) {
return false;
}
return !arrayIsEqualTo(imports1, imports2);
}
/** @internal */
export class TypingsCache {
private readonly perProjectCache = new Map<string, TypingsCacheEntry>();
constructor(private readonly installer: ITypingsInstaller) {
}
isKnownTypesPackageName(name: string): boolean {
return this.installer.isKnownTypesPackageName(name);
}
installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult> {
return this.installer.installPackage(options);
}
enqueueInstallTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray<string> | 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<string>, 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) {
if (this.perProjectCache.delete(project.getProjectName())) {
this.installer.onProjectClosed(project);
}
}
}
+14 -14
View File
@@ -2659,6 +2659,18 @@ declare namespace ts {
trace?(s: string): void;
require?(initialPath: string, moduleName: string): ModuleImportResult;
}
interface InstallPackageOptionsWithProject extends InstallPackageOptions {
projectName: string;
projectRootPath: Path;
}
interface ITypingsInstaller {
isKnownTypesPackageName(name: string): boolean;
installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult>;
enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string> | undefined): void;
attach(projectService: ProjectService): void;
onProjectClosed(p: Project): void;
readonly globalTypingsCacheLocation: string | undefined;
}
function createInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>, cachePath?: string): DiscoverTypings;
function toNormalizedPath(fileName: string): NormalizedPath;
function normalizedPathToPath(normalizedPath: NormalizedPath, currentDirectory: string, getCanonicalFileName: (f: string) => string): Path;
@@ -2751,19 +2763,6 @@ declare namespace ts {
positionToLineOffset(position: number): protocol.Location;
isJavaScript(): boolean;
}
interface InstallPackageOptionsWithProject extends InstallPackageOptions {
projectName: string;
projectRootPath: Path;
}
interface ITypingsInstaller {
isKnownTypesPackageName(name: string): boolean;
installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult>;
enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string> | undefined): void;
attach(projectService: ProjectService): void;
onProjectClosed(p: Project): void;
readonly globalTypingsCacheLocation: string | undefined;
}
const nullTypingsInstaller: ITypingsInstaller;
function allRootFilesAreJsOrDts(project: Project): boolean;
function allFilesAreJsOrDts(project: Project): boolean;
enum ProjectKind {
@@ -2817,6 +2816,7 @@ declare namespace ts {
private lastReportedVersion;
protected projectErrors: Diagnostic[] | undefined;
protected isInitialLoadPending: () => boolean;
private typingsCache;
private typingWatchers;
private readonly cancellationToken;
isNonTsProject(): boolean;
@@ -2829,7 +2829,6 @@ declare namespace ts {
readonly jsDocParsingMode: JSDocParsingMode | undefined;
isKnownTypesPackageName(name: string): boolean;
installPackage(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult>;
private get typingsCache();
getCompilationSettings(): ts.CompilerOptions;
getCompilerOptions(): ts.CompilerOptions;
getNewLine(): string;
@@ -3148,6 +3147,7 @@ declare namespace ts {
configFileName?: NormalizedPath;
configFileErrors?: readonly Diagnostic[];
}
const nullTypingsInstaller: ITypingsInstaller;
interface ProjectServiceOptions {
host: ServerHost;
logger: Logger;