Instead of watching files in typing installer, forward it to projectService (#54028)

This commit is contained in:
Sheetal Nandi
2023-04-26 13:31:36 -07:00
committed by GitHub
parent f22898d577
commit a956bbc831
86 changed files with 1098 additions and 1040 deletions
+4
View File
@@ -692,6 +692,8 @@ export const WatchType: WatchTypeRegistry = {
NoopConfigFileForInferredRoot: "Noop Config file for the inferred project root",
MissingGeneratedFile: "Missing generated file",
NodeModulesForModuleSpecifierCache: "node_modules for module specifier cache invalidation",
TypingInstallerLocationFile: "File location for typing installer",
TypingInstallerLocationDirectory: "Directory location for typing installer",
};
/** @internal */
@@ -717,6 +719,8 @@ export interface WatchTypeRegistry {
NoopConfigFileForInferredRoot: "Noop Config file for the inferred project root",
MissingGeneratedFile: "Missing generated file",
NodeModulesForModuleSpecifierCache: "node_modules for module specifier cache invalidation",
TypingInstallerLocationFile: "File location for typing installer",
TypingInstallerLocationDirectory: "Directory location for typing installer",
}
/** @internal */
+3
View File
@@ -10,6 +10,7 @@ export type EventTypesRegistry = "event::typesRegistry";
export type EventBeginInstallTypes = "event::beginInstallTypes";
export type EventEndInstallTypes = "event::endInstallTypes";
export type EventInitializationFailed = "event::initializationFailed";
export type ActionWatchTypingLocations = "action::watchTypingLocations";
/** @internal */
export const ActionSet: ActionSet = "action::set";
/** @internal */
@@ -24,6 +25,8 @@ export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInsta
export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes";
/** @internal */
export const EventInitializationFailed: EventInitializationFailed = "event::initializationFailed";
/** @internal */
export const ActionWatchTypingLocations: ActionWatchTypingLocations = "action::watchTypingLocations";
/** @internal */
export namespace Arguments {
+9 -9
View File
@@ -1,19 +1,16 @@
import {
CompilerOptions,
DirectoryWatcherCallback,
FileWatcher,
FileWatcherCallback,
JsTyping,
MapLike,
Path,
SortedReadonlyArray,
TypeAcquisition,
WatchOptions,
} from "./_namespaces/ts";
import {
ActionInvalidate,
ActionPackageInstalled,
ActionSet,
ActionWatchTypingLocations,
EventBeginInstallTypes,
EventEndInstallTypes,
EventInitializationFailed,
@@ -21,7 +18,7 @@ import {
} from "./_namespaces/ts.server";
export interface TypingInstallerResponse {
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | ActionPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | ActionPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed | ActionWatchTypingLocations;
}
export interface TypingInstallerRequestWithProjectName {
@@ -35,7 +32,6 @@ export interface DiscoverTypings extends TypingInstallerRequestWithProjectName {
readonly fileNames: string[];
readonly projectRootPath: Path;
readonly compilerOptions: CompilerOptions;
readonly watchOptions?: WatchOptions;
readonly typeAcquisition: TypeAcquisition;
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly cachePath?: string;
@@ -104,8 +100,6 @@ export interface InstallTypingHost extends JsTyping.TypingResolutionHost {
writeFile(path: string, content: string): void;
createDirectory(path: string): void;
getCurrentDirectory?(): string;
watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher;
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher;
}
export interface SetTypings extends ProjectResponse {
@@ -116,5 +110,11 @@ export interface SetTypings extends ProjectResponse {
readonly kind: ActionSet;
}
export interface WatchTypingLocations extends ProjectResponse {
/** if files is undefined, retain same set of watchers */
readonly files: readonly string[] | undefined;
readonly kind: ActionWatchTypingLocations;
}
/** @internal */
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse;
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse | WatchTypingLocations;
+6
View File
@@ -176,6 +176,7 @@ import {
ThrottledOperations,
toNormalizedPath,
TypingsCache,
WatchTypingLocations,
} from "./_namespaces/ts.server";
import * as protocol from "./protocol";
@@ -1153,6 +1154,11 @@ export class ProjectService {
}
}
/** @internal */
watchTypingLocations(response: WatchTypingLocations) {
this.findProject(response.projectName)?.watchTypingLocations(response.files);
}
/** @internal */
delayEnsureProjectForOpenFiles() {
if (!this.openFiles.size) return;
+119
View File
@@ -13,16 +13,19 @@ import {
closeFileWatcher,
closeFileWatcherOf,
combinePaths,
comparePaths,
CompilerHost,
CompilerOptions,
concatenate,
ConfigFileProgramReloadLevel,
containsPath,
createCacheableExportInfoMap,
createLanguageService,
createResolutionCache,
createSymlinkCache,
Debug,
Diagnostic,
directorySeparator,
DirectoryStructureHost,
DirectoryWatcherCallback,
DocumentPositionMapper,
@@ -45,6 +48,7 @@ import {
generateDjb2Hash,
getAllowJSCompilerOption,
getAutomaticTypeDirectiveNames,
getBaseFileName,
GetCanonicalFileName,
getDeclarationEmitOutputFilePathWorker,
getDefaultCompilerOptions,
@@ -126,6 +130,7 @@ import {
WatchType,
} from "./_namespaces/ts";
import {
ActionInvalidate,
asNormalizedPath,
createModuleSpecifierCache,
emptyArray,
@@ -287,6 +292,14 @@ export interface EmitResult {
diagnostics: readonly Diagnostic[];
}
const enum TypingWatcherType {
FileWatcher = "FileWatcher",
DirectoryWatcher = "DirectoryWatcher"
}
type TypingWatchers = Map<Path, FileWatcher> & { isInvoked?: boolean; };
export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap = new Map<string, ProjectRootFile>();
@@ -370,6 +383,9 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
/** @internal */
typingFiles: SortedReadonlyArray<string> = emptyArray;
/** @internal */
private typingWatchers: TypingWatchers | undefined;
/** @internal */
originalConfiguredProjects: Set<NormalizedPath> | undefined;
@@ -1013,6 +1029,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
close() {
this.projectService.typingsCache.onProjectClosed(this);
this.closeWatchingTypingLocations();
if (this.program) {
// if we have a program - release all files that are enlisted in program but arent root
// The releasing of the roots happens later
@@ -1361,6 +1378,108 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
}
/** @internal */
private closeWatchingTypingLocations() {
if (this.typingWatchers) clearMap(this.typingWatchers, closeFileWatcher);
this.typingWatchers = undefined;
}
/** @internal */
private onTypingInstallerWatchInvoke() {
this.typingWatchers!.isInvoked = true;
this.projectService.updateTypingsForProject({ projectName: this.getProjectName(), kind: ActionInvalidate });
}
/** @internal */
watchTypingLocations(files: readonly string[] | undefined) {
if (!files) {
this.typingWatchers!.isInvoked = false;
return;
}
if (!files.length) {
// shut down existing watchers
this.closeWatchingTypingLocations();
return;
}
const toRemove = new Map(this.typingWatchers);
if (!this.typingWatchers) this.typingWatchers = new Map();
// handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
this.typingWatchers.isInvoked = false;
const createProjectWatcher = (path: string, typingsWatcherType: TypingWatcherType) => {
const canonicalPath = this.toPath(path);
toRemove.delete(canonicalPath);
if (!this.typingWatchers!.has(canonicalPath)) {
this.typingWatchers!.set(canonicalPath, typingsWatcherType === TypingWatcherType.FileWatcher ?
this.projectService.watchFactory.watchFile(
path,
() => !this.typingWatchers!.isInvoked ?
this.onTypingInstallerWatchInvoke() :
this.writeLog(`TypingWatchers already invoked`),
PollingInterval.High,
this.projectService.getWatchOptions(this),
WatchType.TypingInstallerLocationFile,
this,
) :
this.projectService.watchFactory.watchDirectory(
path,
f => {
if (this.typingWatchers!.isInvoked) return this.writeLog(`TypingWatchers already invoked`);
if (!fileExtensionIs(f, Extension.Json)) return this.writeLog(`Ignoring files that are not *.json`);
if (comparePaths(f, combinePaths(this.projectService.typingsInstaller.globalTypingsCacheLocation!, "package.json"), !this.useCaseSensitiveFileNames())) return this.writeLog(`Ignoring package.json change at global typings location`);
this.onTypingInstallerWatchInvoke();
},
WatchDirectoryFlags.Recursive,
this.projectService.getWatchOptions(this),
WatchType.TypingInstallerLocationDirectory,
this,
)
);
}
};
// Create watches from list of files
for (const file of files) {
const basename = getBaseFileName(file);
if (basename === "package.json" || basename === "bower.json") {
// package.json or bower.json exists, watch the file to detect changes and update typings
createProjectWatcher(file, TypingWatcherType.FileWatcher);
continue;
}
// path in projectRoot, watch project root
if (containsPath(this.currentDirectory, file, this.currentDirectory, !this.useCaseSensitiveFileNames())) {
const subDirectory = file.indexOf(directorySeparator, this.currentDirectory.length + 1);
if (subDirectory !== -1) {
// Watch subDirectory
createProjectWatcher(file.substr(0, subDirectory), TypingWatcherType.DirectoryWatcher);
}
else {
// Watch the directory itself
createProjectWatcher(file, TypingWatcherType.DirectoryWatcher);
}
continue;
}
// path in global cache, watch global cache
if (containsPath(this.projectService.typingsInstaller.globalTypingsCacheLocation!, file, this.currentDirectory, !this.useCaseSensitiveFileNames())) {
createProjectWatcher(this.projectService.typingsInstaller.globalTypingsCacheLocation!, TypingWatcherType.DirectoryWatcher);
continue;
}
// watch node_modules or bower_components
createProjectWatcher(file, TypingWatcherType.DirectoryWatcher);
}
// Remove unused watches
toRemove.forEach((watch, path) => {
watch.close();
this.typingWatchers!.delete(path);
});
}
/** @internal */
getCurrentProgram(): Program | undefined {
return this.program;
-1
View File
@@ -46,7 +46,6 @@ export function createInstallTypingsRequest(project: Project, typeAcquisition: T
projectName: project.getProjectName(),
fileNames: project.getFileNames(/*excludeFilesFromExternalLibraries*/ true, /*excludeConfigFiles*/ true).concat(project.getExcludedFiles() as NormalizedPath[]),
compilerOptions: project.getCompilationSettings(),
watchOptions: project.projectService.getWatchOptions(project),
typeAcquisition,
unresolvedImports,
projectRootPath: project.getCurrentDirectory() as Path,
+4 -2
View File
@@ -1,5 +1,6 @@
import * as Harness from "../../_namespaces/Harness";
import * as ts from "../../_namespaces/ts";
import { ActionWatchTypingLocations } from "../../_namespaces/ts.server";
import { ensureErrorFreeBuild } from "./solutionBuilder";
import {
changeToHostTrackingWrittenFiles,
@@ -280,11 +281,12 @@ export class TestTypingsInstallerWorker extends ts.server.typingsInstaller.Typin
this.addPostExecAction("success", requestId, packageNames, cb);
}
sendResponse(response: ts.server.SetTypings | ts.server.InvalidateCachedTypings) {
sendResponse(response: ts.server.SetTypings | ts.server.InvalidateCachedTypings | ts.server.WatchTypingLocations) {
if (this.log.isEnabled()) {
this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`);
}
this.projectService.updateTypingsForProject(response);
if (response.kind !== ActionWatchTypingLocations) this.projectService.updateTypingsForProject(response);
else this.projectService.watchTypingLocations(response);
}
enqueueInstallTypingsRequest(project: ts.server.Project, typeAcquisition: ts.TypeAcquisition, unresolvedImports: ts.SortedReadonlyArray<string>) {
+5 -1
View File
@@ -36,6 +36,7 @@ import {
ActionInvalidate,
ActionPackageInstalled,
ActionSet,
ActionWatchTypingLocations,
Arguments,
BeginInstallTypes,
createInstallTypingsRequest,
@@ -669,7 +670,7 @@ function startNodeSession(options: StartSessionOptions, logger: Logger, cancella
}
}
private handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) {
private handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse | server.WatchTypingLocations) {
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Received response:${stringifyIndented(response)}`);
}
@@ -765,6 +766,9 @@ function startNodeSession(options: StartSessionOptions, logger: Logger, cancella
break;
}
case ActionWatchTypingLocations:
this.projectService.watchTypingLocations(response);
break;
default:
assertType<never>(response);
}
+17 -143
View File
@@ -1,21 +1,9 @@
import {
clearMap,
closeFileWatcher,
combinePaths,
compareStringsCaseInsensitive,
Comparison,
containsPath,
copyEntries,
createGetCanonicalFileName,
directorySeparator,
Extension,
fileExtensionIs,
FileWatcher,
forEachKey,
getBaseFileName,
GetCanonicalFileName,
getDirectoryPath,
getProperty,
getWatchFactory,
hasProperty,
JsTyping,
mangleScopedPackageName,
@@ -24,20 +12,14 @@ import {
ModuleResolutionKind,
noop,
Path,
PollingInterval,
resolveModuleName,
Version,
version,
versionMajorMinor,
WatchDirectoryFlags,
WatchFactory,
WatchFactoryHost,
WatchLogLevel,
WatchOptions,
} from "./_namespaces/ts";
import {
ActionInvalidate,
ActionSet,
ActionWatchTypingLocations,
BeginInstallTypes,
CloseProject,
DiscoverTypings,
@@ -47,6 +29,7 @@ import {
InstallTypingHost,
InvalidateCachedTypings,
SetTypings,
WatchTypingLocations,
} from "./_namespaces/ts.server";
interface NpmConfig {
@@ -115,49 +98,19 @@ export interface PendingRequest {
onRequestCompleted: RequestCompletedAction;
}
function endsWith(str: string, suffix: string, caseSensitive: boolean): boolean {
const expectedPos = str.length - suffix.length;
return expectedPos >= 0 &&
(str.indexOf(suffix, expectedPos) === expectedPos ||
(!caseSensitive && compareStringsCaseInsensitive(str.substr(expectedPos), suffix) === Comparison.EqualTo));
}
function isPackageOrBowerJson(fileName: string, caseSensitive: boolean) {
return endsWith(fileName, "/package.json", caseSensitive) || endsWith(fileName, "/bower.json", caseSensitive);
}
function sameFiles(a: string, b: string, caseSensitive: boolean) {
return a === b || (!caseSensitive && compareStringsCaseInsensitive(a, b) === Comparison.EqualTo);
}
const enum ProjectWatcherType {
FileWatcher = "FileWatcher",
DirectoryWatcher = "DirectoryWatcher"
}
type ProjectWatchers = Map<string, FileWatcher> & { isInvoked?: boolean; };
function getDetailWatchInfo(projectName: string, watchers: ProjectWatchers | undefined) {
return `Project: ${projectName} watcher already invoked: ${watchers?.isInvoked}`;
}
export abstract class TypingsInstaller {
private readonly packageNameToTypingLocation = new Map<string, JsTyping.CachedTyping>();
private readonly missingTypingsSet = new Set<string>();
private readonly knownCachesSet = new Set<string>();
private readonly projectWatchers = new Map<string, ProjectWatchers>();
private readonly projectWatchers = new Map<string, Set<string>>();
private safeList: JsTyping.SafeList | undefined;
/** @internal */
readonly pendingRunRequests: PendingRequest[] = [];
private readonly toCanonicalFileName: GetCanonicalFileName;
private readonly globalCachePackageJsonPath: string;
private installRunCount = 1;
private inFlightRequestCount = 0;
abstract readonly typesRegistry: Map<string, MapLike<string>>;
/** @internal */
private readonly watchFactory: WatchFactory<string, ProjectWatchers>;
constructor(
protected readonly installTypingHost: InstallTypingHost,
@@ -166,13 +119,10 @@ export abstract class TypingsInstaller {
private readonly typesMapLocation: Path,
private readonly throttleLimit: number,
protected readonly log = nullLog) {
this.toCanonicalFileName = createGetCanonicalFileName(installTypingHost.useCaseSensitiveFileNames);
this.globalCachePackageJsonPath = combinePaths(globalCachePath, "package.json");
const isLoggingEnabled = this.log.isEnabled();
if (isLoggingEnabled) {
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}', types map path ${typesMapLocation}`);
}
this.watchFactory = getWatchFactory(this.installTypingHost as WatchFactoryHost, isLoggingEnabled ? WatchLogLevel.Verbose : WatchLogLevel.None, s => this.log.writeLine(s), getDetailWatchInfo);
this.processCacheLocation(this.globalCachePath);
}
@@ -191,14 +141,16 @@ export abstract class TypingsInstaller {
}
return;
}
clearMap(watchers, closeFileWatcher);
// Close all the watchers
this.projectWatchers.delete(projectName);
this.sendResponse({ kind: ActionWatchTypingLocations, projectName, files: [] });
if (this.log.isEnabled()) {
this.log.writeLine(`Closing file watchers for project '${projectName}' - done.`);
}
}
install(req: DiscoverTypings) {
if (this.log.isEnabled()) {
this.log.writeLine(`Got install request ${JSON.stringify(req)}`);
@@ -232,7 +184,7 @@ export abstract class TypingsInstaller {
}
// start watching files
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch, req.projectRootPath, req.watchOptions);
this.watchFiles(req.projectName, discoverTypingsResult.filesToWatch);
// install typings
if (discoverTypingsResult.newTypingNames.length) {
@@ -456,101 +408,23 @@ export abstract class TypingsInstaller {
}
}
private watchFiles(projectName: string, files: string[], projectRootPath: Path, options: WatchOptions | undefined) {
private watchFiles(projectName: string, files: string[]) {
if (!files.length) {
// shut down existing watchers
this.closeWatchers(projectName);
return;
}
let watchers = this.projectWatchers.get(projectName)!;
const toRemove = new Map<string, FileWatcher>();
if (!watchers) {
watchers = new Map();
this.projectWatchers.set(projectName, watchers);
const existing = this.projectWatchers.get(projectName);
const newSet = new Set(files);
if (!existing || forEachKey(newSet, s => !existing.has(s)) || forEachKey(existing, s => !newSet.has(s))) {
this.projectWatchers.set(projectName, newSet);
this.sendResponse({ kind: ActionWatchTypingLocations, projectName, files });
}
else {
copyEntries(watchers, toRemove);
// Keep same list of files
this.sendResponse({ kind: ActionWatchTypingLocations, projectName, files: undefined });
}
// handler should be invoked once for the entire set of files since it will trigger full rediscovery of typings
watchers.isInvoked = false;
const isLoggingEnabled = this.log.isEnabled();
const createProjectWatcher = (path: string, projectWatcherType: ProjectWatcherType) => {
const canonicalPath = this.toCanonicalFileName(path);
toRemove.delete(canonicalPath);
if (watchers.has(canonicalPath)) {
return;
}
if (isLoggingEnabled) {
this.log.writeLine(`${projectWatcherType}:: Added:: WatchInfo: ${path}`);
}
const watcher = projectWatcherType === ProjectWatcherType.FileWatcher ?
this.watchFactory.watchFile(path, () => {
if (!watchers.isInvoked) {
watchers.isInvoked = true;
this.sendResponse({ projectName, kind: ActionInvalidate });
}
}, PollingInterval.High, options, projectName, watchers) :
this.watchFactory.watchDirectory(path, f => {
if (watchers.isInvoked || !fileExtensionIs(f, Extension.Json)) {
return;
}
if (isPackageOrBowerJson(f, this.installTypingHost.useCaseSensitiveFileNames) &&
!sameFiles(f, this.globalCachePackageJsonPath, this.installTypingHost.useCaseSensitiveFileNames)) {
watchers.isInvoked = true;
this.sendResponse({ projectName, kind: ActionInvalidate });
}
}, WatchDirectoryFlags.Recursive, options, projectName, watchers);
watchers.set(canonicalPath, isLoggingEnabled ? {
close: () => {
this.log.writeLine(`${projectWatcherType}:: Closed:: WatchInfo: ${path}`);
watcher.close();
}
} : watcher);
};
// Create watches from list of files
for (const file of files) {
if (file.endsWith("/package.json") || file.endsWith("/bower.json")) {
// package.json or bower.json exists, watch the file to detect changes and update typings
createProjectWatcher(file, ProjectWatcherType.FileWatcher);
continue;
}
// path in projectRoot, watch project root
if (containsPath(projectRootPath, file, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) {
const subDirectory = file.indexOf(directorySeparator, projectRootPath.length + 1);
if (subDirectory !== -1) {
// Watch subDirectory
createProjectWatcher(file.substr(0, subDirectory), ProjectWatcherType.DirectoryWatcher);
}
else {
// Watch the directory itself
createProjectWatcher(file, ProjectWatcherType.DirectoryWatcher);
}
continue;
}
// path in global cache, watch global cache
if (containsPath(this.globalCachePath, file, projectRootPath, !this.installTypingHost.useCaseSensitiveFileNames)) {
createProjectWatcher(this.globalCachePath, ProjectWatcherType.DirectoryWatcher);
continue;
}
// watch node_modules or bower_components
createProjectWatcher(file, ProjectWatcherType.DirectoryWatcher);
}
// Remove unused watches
toRemove.forEach((watch, path) => {
watch.close();
watchers.delete(path);
});
}
private createSetTypings(request: DiscoverTypings, typings: string[]): SetTypings {
@@ -582,7 +456,7 @@ export abstract class TypingsInstaller {
}
protected abstract installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void;
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void;
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | WatchTypingLocations): void;
protected readonly latestDistTag = "latest";
}