Skip to content

Commit 787c995

Browse files
committed
Allow recursive directory watching on non supported file system
1 parent bcfa02f commit 787c995

8 files changed

Lines changed: 183 additions & 60 deletions

File tree

src/compiler/core.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ namespace ts {
2020

2121
/* @internal */
2222
namespace ts {
23+
export const emptyArray: never[] = [] as never[];
24+
25+
export function closeFileWatcher(watcher: FileWatcher) {
26+
watcher.close();
27+
}
28+
2329
/** Create a MapLike with good performance. */
2430
function createDictionaryObject<T>(): MapLike<T> {
2531
const map = Object.create(/*prototype*/ null); // tslint:disable-line:no-null-keyword
@@ -3270,4 +3276,36 @@ namespace ts {
32703276
export function singleElementArray<T>(t: T | undefined): T[] | undefined {
32713277
return t === undefined ? undefined : [t];
32723278
}
3279+
3280+
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) {
3281+
unchanged = unchanged || noop;
3282+
let newIndex = 0;
3283+
let oldIndex = 0;
3284+
const newLen = newItems.length;
3285+
const oldLen = oldItems.length;
3286+
while (newIndex < newLen && oldIndex < oldLen) {
3287+
const newItem = newItems[newIndex];
3288+
const oldItem = oldItems[oldIndex];
3289+
const compareResult = comparer(newItem, oldItem);
3290+
if (compareResult === Comparison.LessThan) {
3291+
inserted(newItem);
3292+
newIndex++;
3293+
}
3294+
else if (compareResult === Comparison.GreaterThan) {
3295+
deleted(oldItem);
3296+
oldIndex++;
3297+
}
3298+
else {
3299+
unchanged(oldItem, newItem);
3300+
newIndex++;
3301+
oldIndex++;
3302+
}
3303+
}
3304+
while (newIndex < newLen) {
3305+
inserted(newItems[newIndex++]);
3306+
}
3307+
while (oldIndex < oldLen) {
3308+
deleted(oldItems[oldIndex++]);
3309+
}
3310+
}
32733311
}

src/compiler/sys.ts

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ namespace ts {
5757

5858
/* @internal */
5959
export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval) => FileWatcher;
60+
/* @internal */
61+
export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive?: boolean) => FileWatcher;
6062

6163
/* @internal */
6264
export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time
@@ -286,6 +288,93 @@ namespace ts {
286288
return false;
287289
}
288290

291+
/*@internal*/
292+
export interface RecursiveDirectoryWatcherHost {
293+
watchDirectory: HostWatchDirectory;
294+
getAccessileSortedChildDirectories(path: string): ReadonlyArray<string>;
295+
filePathComparer: Comparer<string>;
296+
}
297+
298+
/**
299+
* Watch the directory recursively using host provided method to watch child directories
300+
* that means if this is recursive watcher, watch the children directories as well
301+
* (eg on OS that dont support recursive watch using fs.watch use fs.watchFile)
302+
*/
303+
/*@internal*/
304+
export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher {
305+
type ChildWatches = ReadonlyArray<DirectoryWatcher>;
306+
interface DirectoryWatcher extends FileWatcher {
307+
childWatches: ChildWatches;
308+
dirName: string;
309+
}
310+
311+
return createDirectoryWatcher;
312+
313+
/**
314+
* Create the directory watcher for the dirPath.
315+
*/
316+
function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher {
317+
const watcher = host.watchDirectory(dirName, fileName => {
318+
// Call the actual callback
319+
callback(fileName);
320+
321+
// Iterate through existing children and update the watches if needed
322+
updateChildWatches(result, callback);
323+
});
324+
325+
let result: DirectoryWatcher = {
326+
close: () => {
327+
watcher.close();
328+
result.childWatches.forEach(closeFileWatcher);
329+
result = undefined;
330+
},
331+
dirName,
332+
childWatches: emptyArray
333+
};
334+
updateChildWatches(result, callback);
335+
return result;
336+
}
337+
338+
function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) {
339+
// Iterate through existing children and update the watches if needed
340+
if (watcher) {
341+
watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback);
342+
}
343+
}
344+
345+
/**
346+
* Watch the directories in the parentDir
347+
*/
348+
function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches {
349+
let newChildWatches: DirectoryWatcher[] | undefined;
350+
enumerateInsertsAndDeletes<string, DirectoryWatcher>(
351+
host.getAccessileSortedChildDirectories(parentDir),
352+
existingChildWatches,
353+
(child, childWatcher) => host.filePathComparer(getNormalizedAbsolutePath(child, parentDir), childWatcher.dirName),
354+
createAndAddChildDirectoryWatcher,
355+
closeFileWatcher,
356+
addChildDirectoryWatcher
357+
);
358+
359+
return newChildWatches || emptyArray;
360+
361+
/**
362+
* Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list
363+
*/
364+
function createAndAddChildDirectoryWatcher(childName: string) {
365+
const result = createDirectoryWatcher(getNormalizedAbsolutePath(childName, parentDir), callback);
366+
addChildDirectoryWatcher(result);
367+
}
368+
369+
/**
370+
* Add child directory watcher to the new ChildDirectoryWatcher list
371+
*/
372+
function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) {
373+
(newChildWatches || (newChildWatches = [])).push(childWatcher);
374+
}
375+
}
376+
}
377+
289378
/**
290379
* Partial interface of the System thats needed to support the caching of directory structure
291380
*/
@@ -402,7 +491,8 @@ namespace ts {
402491
}
403492

404493
const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER;
405-
const tscWatchOption = process.env.TSC_WATCHOPTION;
494+
const tscWatchFile = process.env.TSC_WATCHFILE;
495+
const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY;
406496

407497
const nodeSystem: System = {
408498
args: process.argv.slice(2),
@@ -483,19 +573,7 @@ namespace ts {
483573
}
484574
};
485575
nodeSystem.watchFile = getWatchFile();
486-
nodeSystem.watchDirectory = (directoryName, callback, recursive) => {
487-
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
488-
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
489-
return fsWatchDirectory(directoryName, (eventName, relativeFileName) => {
490-
// In watchDirectory we only care about adding and removing files (when event name is
491-
// "rename"); changes made within files are handled by corresponding fileWatchers (when
492-
// event name is "change")
493-
if (eventName === "rename") {
494-
// When deleting a file, the passed baseFileName is null
495-
callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName)));
496-
}
497-
}, recursive);
498-
};
576+
nodeSystem.watchDirectory = getWatchDirectory();
499577
return nodeSystem;
500578

501579
function isFileSystemCaseSensitive(): boolean {
@@ -516,7 +594,7 @@ namespace ts {
516594
}
517595

518596
function getWatchFile(): HostWatchFile {
519-
switch (tscWatchOption) {
597+
switch (tscWatchFile) {
520598
case "PriorityPollingInterval":
521599
// Use polling interval based on priority when create watch using host.watchFile
522600
return fsWatchFile;
@@ -536,6 +614,29 @@ namespace ts {
536614
(fileName, callback) => fsWatchFile(fileName, callback);
537615
}
538616

617+
function getWatchDirectory(): HostWatchDirectory {
618+
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
619+
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
620+
const fsSupportsRecursive = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin");
621+
if (fsSupportsRecursive) {
622+
return watchDirectoryUsingFsWatch;
623+
}
624+
625+
const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? watchDirectoryUsingFsWatchFile : watchDirectoryUsingFsWatch;
626+
const watchDirectoryRecursively = createRecursiveDirectoryWatcher({
627+
filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive,
628+
getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories,
629+
watchDirectory
630+
});
631+
632+
return (directoryName, callback, recursive) => {
633+
if (recursive) {
634+
return watchDirectoryRecursively(directoryName, callback);
635+
}
636+
watchDirectory(directoryName, callback);
637+
};
638+
}
639+
539640
function createNonPollingWatchFile() {
540641
// One file can have multiple watchers
541642
const fileWatcherCallbacks = createMultiMap<FileWatcherCallback>();
@@ -616,11 +717,11 @@ namespace ts {
616717

617718
type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string) => void;
618719

619-
function createFsWatchFileCallback(callback: FsWatchCallback): FileWatcherCallback {
720+
function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback {
620721
return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "");
621722
}
622723

623-
function createFsWatchCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback {
724+
function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback {
624725
return eventName => {
625726
if (eventName === "rename") {
626727
callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted);
@@ -632,6 +733,18 @@ namespace ts {
632733
};
633734
}
634735

736+
function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback {
737+
return (eventName, relativeFileName) => {
738+
// In watchDirectory we only care about adding and removing files (when event name is
739+
// "rename"); changes made within files are handled by corresponding fileWatchers (when
740+
// event name is "change")
741+
if (eventName === "rename") {
742+
// When deleting a file, the passed baseFileName is null
743+
callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName)));
744+
}
745+
};
746+
}
747+
635748
function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher {
636749
let options: any;
637750
/** Watcher for the file system entry depending on whether it is missing or present */
@@ -700,7 +813,7 @@ namespace ts {
700813
* 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
701814
*/
702815
function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher {
703-
return fallbackPollingWatchFile(fileOrDirectory, createFsWatchFileCallback(callback), pollingInterval);
816+
return fallbackPollingWatchFile(fileOrDirectory, createFileWatcherCallback(callback), pollingInterval);
704817
}
705818

706819
/**
@@ -720,18 +833,26 @@ namespace ts {
720833
}
721834

722835
function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) {
723-
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval);
836+
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval);
724837
}
725838

726839
function watchFileUsingDynamicWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) {
727840
const watchFile = createDynamicPriorityPollingWatchFile(nodeSystem);
728-
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval);
841+
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval);
729842
}
730843

731844
function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher {
732845
return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile);
733846
}
734847

848+
function watchDirectoryUsingFsWatch(directoryName: string, callback: DirectoryWatcherCallback, recursive?: boolean) {
849+
return fsWatchDirectory(directoryName, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive);
850+
}
851+
852+
function watchDirectoryUsingFsWatchFile(directoryName: string, callback: DirectoryWatcherCallback) {
853+
return fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium);
854+
}
855+
735856
function readFile(fileName: string, _encoding?: string): string | undefined {
736857
if (!fileExists(fileName)) {
737858
return undefined;

src/compiler/utilities.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
/* @internal */
44
namespace ts {
5-
export const emptyArray: never[] = [] as never[];
65
export const resolvingEmptyArray: never[] = [] as never[];
76
export const emptyMap: ReadonlyMap<never> = createMap<never>();
87
export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap<never> = emptyMap as ReadonlyUnderscoreEscapedMap<never>;

src/compiler/watchUtilities.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,6 @@ namespace ts {
181181
return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}`;
182182
}
183183

184-
export function closeFileWatcher(watcher: FileWatcher) {
185-
watcher.close();
186-
}
187-
188184
export function closeFileWatcherOf<T extends { watcher: FileWatcher; }>(objWithWatcher: T) {
189185
objWithWatcher.watcher.close();
190186
}

src/harness/unittests/tscWatchMode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2123,7 +2123,7 @@ declare module "fs" {
21232123
};
21242124
const files = [file1, libFile];
21252125
const environmentVariables = createMap<string>();
2126-
environmentVariables.set("TSC_WATCHOPTION", "DynamicPriorityPolling");
2126+
environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling");
21272127
const host = createWatchedSystem(files, { environmentVariables });
21282128
const watch = createWatchModeWithoutConfigFile([file1.path], host);
21292129

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ interface Array<T> {}`
270270
this.executingFilePath = this.getHostSpecificPath(executingFilePath);
271271
this.currentDirectory = this.getHostSpecificPath(currentDirectory);
272272
this.reloadFS(fileOrFolderList);
273-
this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHOPTION") === "DynamicPriorityPolling" ?
273+
this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ?
274274
createDynamicPriorityPollingWatchFile(this) :
275275
undefined;
276276
}

src/server/project.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -891,16 +891,15 @@ namespace ts.server {
891891

892892
const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray<string>;
893893
this.externalFiles = this.getExternalFiles();
894-
enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles,
894+
enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, compareStringsCaseSensitive,
895895
// Ensure a ScriptInfo is created for new external files. This is performed indirectly
896896
// by the LSHost for files in the program when the program is retrieved above but
897897
// the program doesn't contain external files so this must be done explicitly.
898898
inserted => {
899899
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost);
900900
scriptInfo.attachToProject(this);
901901
},
902-
removed => this.detachScriptInfoFromProject(removed),
903-
compareStringsCaseSensitive
902+
removed => this.detachScriptInfoFromProject(removed)
904903
);
905904
const elapsed = timestamp() - start;
906905
this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`);

src/server/utilities.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -276,36 +276,6 @@ namespace ts.server {
276276
return index === 0 || value !== array[index - 1];
277277
}
278278

279-
export function enumerateInsertsAndDeletes<T>(newItems: SortedReadonlyArray<T>, oldItems: SortedReadonlyArray<T>, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, comparer: Comparer<T>) {
280-
let newIndex = 0;
281-
let oldIndex = 0;
282-
const newLen = newItems.length;
283-
const oldLen = oldItems.length;
284-
while (newIndex < newLen && oldIndex < oldLen) {
285-
const newItem = newItems[newIndex];
286-
const oldItem = oldItems[oldIndex];
287-
const compareResult = comparer(newItem, oldItem);
288-
if (compareResult === Comparison.LessThan) {
289-
inserted(newItem);
290-
newIndex++;
291-
}
292-
else if (compareResult === Comparison.GreaterThan) {
293-
deleted(oldItem);
294-
oldIndex++;
295-
}
296-
else {
297-
newIndex++;
298-
oldIndex++;
299-
}
300-
}
301-
while (newIndex < newLen) {
302-
inserted(newItems[newIndex++]);
303-
}
304-
while (oldIndex < oldLen) {
305-
deleted(oldItems[oldIndex++]);
306-
}
307-
}
308-
309279
/* @internal */
310280
export function indent(str: string): string {
311281
return "\n " + str;

0 commit comments

Comments
 (0)