mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-18 05:43:11 -05:00
Allow recursive directory watching on non supported file system
This commit is contained in:
@@ -20,6 +20,12 @@ namespace ts {
|
||||
|
||||
/* @internal */
|
||||
namespace ts {
|
||||
export const emptyArray: never[] = [] as never[];
|
||||
|
||||
export function closeFileWatcher(watcher: FileWatcher) {
|
||||
watcher.close();
|
||||
}
|
||||
|
||||
/** Create a MapLike with good performance. */
|
||||
function createDictionaryObject<T>(): MapLike<T> {
|
||||
const map = Object.create(/*prototype*/ null); // tslint:disable-line:no-null-keyword
|
||||
@@ -3270,4 +3276,36 @@ namespace ts {
|
||||
export function singleElementArray<T>(t: T | undefined): T[] | undefined {
|
||||
return t === undefined ? undefined : [t];
|
||||
}
|
||||
|
||||
export function enumerateInsertsAndDeletes<T, U>(newItems: ReadonlyArray<T>, oldItems: ReadonlyArray<U>, comparer: (a: T, b: U) => Comparison, inserted: (newItem: T) => void, deleted: (oldItem: U) => void, unchanged?: (oldItem: U, newItem: T) => void) {
|
||||
unchanged = unchanged || noop;
|
||||
let newIndex = 0;
|
||||
let oldIndex = 0;
|
||||
const newLen = newItems.length;
|
||||
const oldLen = oldItems.length;
|
||||
while (newIndex < newLen && oldIndex < oldLen) {
|
||||
const newItem = newItems[newIndex];
|
||||
const oldItem = oldItems[oldIndex];
|
||||
const compareResult = comparer(newItem, oldItem);
|
||||
if (compareResult === Comparison.LessThan) {
|
||||
inserted(newItem);
|
||||
newIndex++;
|
||||
}
|
||||
else if (compareResult === Comparison.GreaterThan) {
|
||||
deleted(oldItem);
|
||||
oldIndex++;
|
||||
}
|
||||
else {
|
||||
unchanged(oldItem, newItem);
|
||||
newIndex++;
|
||||
oldIndex++;
|
||||
}
|
||||
}
|
||||
while (newIndex < newLen) {
|
||||
inserted(newItems[newIndex++]);
|
||||
}
|
||||
while (oldIndex < oldLen) {
|
||||
deleted(oldItems[oldIndex++]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ namespace ts {
|
||||
|
||||
/* @internal */
|
||||
export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval) => FileWatcher;
|
||||
/* @internal */
|
||||
export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive?: boolean) => FileWatcher;
|
||||
|
||||
/* @internal */
|
||||
export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time
|
||||
@@ -286,6 +288,93 @@ namespace ts {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
export interface RecursiveDirectoryWatcherHost {
|
||||
watchDirectory: HostWatchDirectory;
|
||||
getAccessileSortedChildDirectories(path: string): ReadonlyArray<string>;
|
||||
filePathComparer: Comparer<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the directory recursively using host provided method to watch child directories
|
||||
* that means if this is recursive watcher, watch the children directories as well
|
||||
* (eg on OS that dont support recursive watch using fs.watch use fs.watchFile)
|
||||
*/
|
||||
/*@internal*/
|
||||
export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher {
|
||||
type ChildWatches = ReadonlyArray<DirectoryWatcher>;
|
||||
interface DirectoryWatcher extends FileWatcher {
|
||||
childWatches: ChildWatches;
|
||||
dirName: string;
|
||||
}
|
||||
|
||||
return createDirectoryWatcher;
|
||||
|
||||
/**
|
||||
* Create the directory watcher for the dirPath.
|
||||
*/
|
||||
function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher {
|
||||
const watcher = host.watchDirectory(dirName, fileName => {
|
||||
// Call the actual callback
|
||||
callback(fileName);
|
||||
|
||||
// Iterate through existing children and update the watches if needed
|
||||
updateChildWatches(result, callback);
|
||||
});
|
||||
|
||||
let result: DirectoryWatcher = {
|
||||
close: () => {
|
||||
watcher.close();
|
||||
result.childWatches.forEach(closeFileWatcher);
|
||||
result = undefined;
|
||||
},
|
||||
dirName,
|
||||
childWatches: emptyArray
|
||||
};
|
||||
updateChildWatches(result, callback);
|
||||
return result;
|
||||
}
|
||||
|
||||
function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) {
|
||||
// Iterate through existing children and update the watches if needed
|
||||
if (watcher) {
|
||||
watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the directories in the parentDir
|
||||
*/
|
||||
function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches {
|
||||
let newChildWatches: DirectoryWatcher[] | undefined;
|
||||
enumerateInsertsAndDeletes<string, DirectoryWatcher>(
|
||||
host.getAccessileSortedChildDirectories(parentDir),
|
||||
existingChildWatches,
|
||||
(child, childWatcher) => host.filePathComparer(getNormalizedAbsolutePath(child, parentDir), childWatcher.dirName),
|
||||
createAndAddChildDirectoryWatcher,
|
||||
closeFileWatcher,
|
||||
addChildDirectoryWatcher
|
||||
);
|
||||
|
||||
return newChildWatches || emptyArray;
|
||||
|
||||
/**
|
||||
* Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list
|
||||
*/
|
||||
function createAndAddChildDirectoryWatcher(childName: string) {
|
||||
const result = createDirectoryWatcher(getNormalizedAbsolutePath(childName, parentDir), callback);
|
||||
addChildDirectoryWatcher(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add child directory watcher to the new ChildDirectoryWatcher list
|
||||
*/
|
||||
function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) {
|
||||
(newChildWatches || (newChildWatches = [])).push(childWatcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial interface of the System thats needed to support the caching of directory structure
|
||||
*/
|
||||
@@ -402,7 +491,8 @@ namespace ts {
|
||||
}
|
||||
|
||||
const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER;
|
||||
const tscWatchOption = process.env.TSC_WATCHOPTION;
|
||||
const tscWatchFile = process.env.TSC_WATCHFILE;
|
||||
const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY;
|
||||
|
||||
const nodeSystem: System = {
|
||||
args: process.argv.slice(2),
|
||||
@@ -483,19 +573,7 @@ namespace ts {
|
||||
}
|
||||
};
|
||||
nodeSystem.watchFile = getWatchFile();
|
||||
nodeSystem.watchDirectory = (directoryName, callback, recursive) => {
|
||||
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
|
||||
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
|
||||
return fsWatchDirectory(directoryName, (eventName, relativeFileName) => {
|
||||
// In watchDirectory we only care about adding and removing files (when event name is
|
||||
// "rename"); changes made within files are handled by corresponding fileWatchers (when
|
||||
// event name is "change")
|
||||
if (eventName === "rename") {
|
||||
// When deleting a file, the passed baseFileName is null
|
||||
callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName)));
|
||||
}
|
||||
}, recursive);
|
||||
};
|
||||
nodeSystem.watchDirectory = getWatchDirectory();
|
||||
return nodeSystem;
|
||||
|
||||
function isFileSystemCaseSensitive(): boolean {
|
||||
@@ -516,7 +594,7 @@ namespace ts {
|
||||
}
|
||||
|
||||
function getWatchFile(): HostWatchFile {
|
||||
switch (tscWatchOption) {
|
||||
switch (tscWatchFile) {
|
||||
case "PriorityPollingInterval":
|
||||
// Use polling interval based on priority when create watch using host.watchFile
|
||||
return fsWatchFile;
|
||||
@@ -536,6 +614,29 @@ namespace ts {
|
||||
(fileName, callback) => fsWatchFile(fileName, callback);
|
||||
}
|
||||
|
||||
function getWatchDirectory(): HostWatchDirectory {
|
||||
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
|
||||
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
|
||||
const fsSupportsRecursive = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin");
|
||||
if (fsSupportsRecursive) {
|
||||
return watchDirectoryUsingFsWatch;
|
||||
}
|
||||
|
||||
const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? watchDirectoryUsingFsWatchFile : watchDirectoryUsingFsWatch;
|
||||
const watchDirectoryRecursively = createRecursiveDirectoryWatcher({
|
||||
filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
|
||||
getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories,
|
||||
watchDirectory
|
||||
});
|
||||
|
||||
return (directoryName, callback, recursive) => {
|
||||
if (recursive) {
|
||||
return watchDirectoryRecursively(directoryName, callback);
|
||||
}
|
||||
watchDirectory(directoryName, callback);
|
||||
};
|
||||
}
|
||||
|
||||
function createNonPollingWatchFile() {
|
||||
// One file can have multiple watchers
|
||||
const fileWatcherCallbacks = createMultiMap<FileWatcherCallback>();
|
||||
@@ -616,11 +717,11 @@ namespace ts {
|
||||
|
||||
type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string) => void;
|
||||
|
||||
function createFsWatchFileCallback(callback: FsWatchCallback): FileWatcherCallback {
|
||||
function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback {
|
||||
return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "");
|
||||
}
|
||||
|
||||
function createFsWatchCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback {
|
||||
function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback {
|
||||
return eventName => {
|
||||
if (eventName === "rename") {
|
||||
callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted);
|
||||
@@ -632,6 +733,18 @@ namespace ts {
|
||||
};
|
||||
}
|
||||
|
||||
function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback {
|
||||
return (eventName, relativeFileName) => {
|
||||
// In watchDirectory we only care about adding and removing files (when event name is
|
||||
// "rename"); changes made within files are handled by corresponding fileWatchers (when
|
||||
// event name is "change")
|
||||
if (eventName === "rename") {
|
||||
// When deleting a file, the passed baseFileName is null
|
||||
callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher {
|
||||
let options: any;
|
||||
/** Watcher for the file system entry depending on whether it is missing or present */
|
||||
@@ -700,7 +813,7 @@ namespace ts {
|
||||
* Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
|
||||
*/
|
||||
function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher {
|
||||
return fallbackPollingWatchFile(fileOrDirectory, createFsWatchFileCallback(callback), pollingInterval);
|
||||
return fallbackPollingWatchFile(fileOrDirectory, createFileWatcherCallback(callback), pollingInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -720,18 +833,26 @@ namespace ts {
|
||||
}
|
||||
|
||||
function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) {
|
||||
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval);
|
||||
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval);
|
||||
}
|
||||
|
||||
function watchFileUsingDynamicWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) {
|
||||
const watchFile = createDynamicPriorityPollingWatchFile(nodeSystem);
|
||||
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval);
|
||||
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval);
|
||||
}
|
||||
|
||||
function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher {
|
||||
return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile);
|
||||
}
|
||||
|
||||
function watchDirectoryUsingFsWatch(directoryName: string, callback: DirectoryWatcherCallback, recursive?: boolean) {
|
||||
return fsWatchDirectory(directoryName, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive);
|
||||
}
|
||||
|
||||
function watchDirectoryUsingFsWatchFile(directoryName: string, callback: DirectoryWatcherCallback) {
|
||||
return fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium);
|
||||
}
|
||||
|
||||
function readFile(fileName: string, _encoding?: string): string | undefined {
|
||||
if (!fileExists(fileName)) {
|
||||
return undefined;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
/* @internal */
|
||||
namespace ts {
|
||||
export const emptyArray: never[] = [] as never[];
|
||||
export const resolvingEmptyArray: never[] = [] as never[];
|
||||
export const emptyMap: ReadonlyMap<never> = createMap<never>();
|
||||
export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap<never> = emptyMap as ReadonlyUnderscoreEscapedMap<never>;
|
||||
|
||||
@@ -181,10 +181,6 @@ namespace ts {
|
||||
return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}`;
|
||||
}
|
||||
|
||||
export function closeFileWatcher(watcher: FileWatcher) {
|
||||
watcher.close();
|
||||
}
|
||||
|
||||
export function closeFileWatcherOf<T extends { watcher: FileWatcher; }>(objWithWatcher: T) {
|
||||
objWithWatcher.watcher.close();
|
||||
}
|
||||
|
||||
@@ -2123,7 +2123,7 @@ declare module "fs" {
|
||||
};
|
||||
const files = [file1, libFile];
|
||||
const environmentVariables = createMap<string>();
|
||||
environmentVariables.set("TSC_WATCHOPTION", "DynamicPriorityPolling");
|
||||
environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling");
|
||||
const host = createWatchedSystem(files, { environmentVariables });
|
||||
const watch = createWatchModeWithoutConfigFile([file1.path], host);
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ interface Array<T> {}`
|
||||
this.executingFilePath = this.getHostSpecificPath(executingFilePath);
|
||||
this.currentDirectory = this.getHostSpecificPath(currentDirectory);
|
||||
this.reloadFS(fileOrFolderList);
|
||||
this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHOPTION") === "DynamicPriorityPolling" ?
|
||||
this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ?
|
||||
createDynamicPriorityPollingWatchFile(this) :
|
||||
undefined;
|
||||
}
|
||||
|
||||
@@ -891,7 +891,7 @@ namespace ts.server {
|
||||
|
||||
const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray<string>;
|
||||
this.externalFiles = this.getExternalFiles();
|
||||
enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles,
|
||||
enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, compareStringsCaseSensitive,
|
||||
// Ensure a ScriptInfo is created for new external files. This is performed indirectly
|
||||
// by the LSHost for files in the program when the program is retrieved above but
|
||||
// the program doesn't contain external files so this must be done explicitly.
|
||||
@@ -899,8 +899,7 @@ namespace ts.server {
|
||||
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost);
|
||||
scriptInfo.attachToProject(this);
|
||||
},
|
||||
removed => this.detachScriptInfoFromProject(removed),
|
||||
compareStringsCaseSensitive
|
||||
removed => this.detachScriptInfoFromProject(removed)
|
||||
);
|
||||
const elapsed = timestamp() - start;
|
||||
this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`);
|
||||
|
||||
@@ -276,36 +276,6 @@ namespace ts.server {
|
||||
return index === 0 || value !== array[index - 1];
|
||||
}
|
||||
|
||||
export function enumerateInsertsAndDeletes<T>(newItems: SortedReadonlyArray<T>, oldItems: SortedReadonlyArray<T>, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, comparer: Comparer<T>) {
|
||||
let newIndex = 0;
|
||||
let oldIndex = 0;
|
||||
const newLen = newItems.length;
|
||||
const oldLen = oldItems.length;
|
||||
while (newIndex < newLen && oldIndex < oldLen) {
|
||||
const newItem = newItems[newIndex];
|
||||
const oldItem = oldItems[oldIndex];
|
||||
const compareResult = comparer(newItem, oldItem);
|
||||
if (compareResult === Comparison.LessThan) {
|
||||
inserted(newItem);
|
||||
newIndex++;
|
||||
}
|
||||
else if (compareResult === Comparison.GreaterThan) {
|
||||
deleted(oldItem);
|
||||
oldIndex++;
|
||||
}
|
||||
else {
|
||||
newIndex++;
|
||||
oldIndex++;
|
||||
}
|
||||
}
|
||||
while (newIndex < newLen) {
|
||||
inserted(newItems[newIndex++]);
|
||||
}
|
||||
while (oldIndex < oldLen) {
|
||||
deleted(oldItems[oldIndex++]);
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function indent(str: string): string {
|
||||
return "\n " + str;
|
||||
|
||||
Reference in New Issue
Block a user