diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index bdefe6fa344cd..081bc2794acf8 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -3,6 +3,11 @@ namespace ts { export type FileWatcherCallback = (fileName: string, removed?: boolean) => void; export type DirectoryWatcherCallback = (directoryName: string) => void; + export interface WatchedFile { + fileName: string; + callback: FileWatcherCallback; + mtime?: Date; + } export interface System { args: string[]; @@ -223,6 +228,92 @@ namespace ts { const _path = require("path"); const _os = require("os"); const _crypto = require("crypto"); + const _process = require("process"); + + const useNonPollingWatchers = _process.env["TSC_NONPOLLING_WATCHER"]; + + function createWatchedFileSet() { + const dirWatchers: Map = {}; + // One file can have multiple watchers + const fileWatcherCallbacks: Map = {}; + return { addFile, removeFile }; + + function reduceDirWatcherRefCountForFile(fileName: string) { + const dirName = getDirectoryPath(fileName); + if (hasProperty(dirWatchers, dirName)) { + const watcher = dirWatchers[dirName]; + watcher.referenceCount -= 1; + if (watcher.referenceCount <= 0) { + watcher.close(); + delete dirWatchers[dirName]; + } + } + } + + function addDirWatcher(dirPath: string): void { + if (hasProperty(dirWatchers, dirPath)) { + const watcher = dirWatchers[dirPath]; + watcher.referenceCount += 1; + return; + } + + const watcher: DirectoryWatcher = _fs.watch( + dirPath, + { persistent: true }, + (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath) + ); + watcher.referenceCount = 1; + dirWatchers[dirPath] = watcher; + return; + } + + function addFileWatcherCallback(filePath: string, callback: FileWatcherCallback): void { + if (hasProperty(fileWatcherCallbacks, filePath)) { + fileWatcherCallbacks[filePath].push(callback); + } + else { + fileWatcherCallbacks[filePath] = [callback]; + } + } + + function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { + addFileWatcherCallback(fileName, callback); + addDirWatcher(getDirectoryPath(fileName)); + + return { fileName, callback }; + } + + function removeFile(watchedFile: WatchedFile) { + removeFileWatcherCallback(watchedFile.fileName, watchedFile.callback); + reduceDirWatcherRefCountForFile(watchedFile.fileName); + } + + function removeFileWatcherCallback(filePath: string, callback: FileWatcherCallback) { + if (hasProperty(fileWatcherCallbacks, filePath)) { + const newCallbacks = copyListRemovingItem(callback, fileWatcherCallbacks[filePath]); + if (newCallbacks.length === 0) { + delete fileWatcherCallbacks[filePath]; + } + else { + fileWatcherCallbacks[filePath] = newCallbacks; + } + } + } + + function fileEventHandler(eventName: string, relativeFileName: string, baseDirPath: string) { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + const fileName = typeof relativeFileName !== "string" + ? undefined + : ts.getNormalizedAbsolutePath(relativeFileName, baseDirPath); + // Some applications save a working file via rename operations + if ((eventName === "change" || eventName === "rename") && hasProperty(fileWatcherCallbacks, fileName)) { + for (const fileCallback of fileWatcherCallbacks[fileName]) { + fileCallback(fileName); + } + } + } + } + const watchedFileSet = createWatchedFileSet(); function isNode4OrLater(): boolean { return parseInt(process.version.charAt(1)) >= 4; @@ -319,7 +410,7 @@ namespace ts { const files = _fs.readdirSync(path || ".").sort(); const directories: string[] = []; for (const current of files) { - // This is necessary because on some file system node fails to exclude + // This is necessary because on some file system node fails to exclude // "." and "..". See https://github.com/nodejs/node/issues/4002 if (current === "." || current === "..") { continue; @@ -353,7 +444,26 @@ namespace ts { readFile, writeFile, watchFile: (fileName, callback) => { - return _fs.watchFile(fileName, callback); + if (useNonPollingWatchers) { + const watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: () => watchedFileSet.removeFile(watchedFile) + }; + } + else { + _fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); + return { + close: () => _fs.unwatchFile(fileName, fileChanged) + }; + } + + function fileChanged(curr: any, prev: any) { + if (+curr.mtime <= +prev.mtime) { + return; + } + + callback(fileName); + } }, watchDirectory: (directoryName, callback, recursive) => { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c39a4b10cbf1c..fc25073052f60 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2104,7 +2104,7 @@ namespace ts { jsxFlags?: JsxFlags; // flags for knowing what kind of element/attributes we're dealing with resolvedJsxType?: Type; // resolved element attributes type of a JSX openinglike element hasSuperCall?: boolean; // recorded result when we try to find super-call. We only try to find one if this flag is undefined, indicating that we haven't made an attempt. - superCall?: ExpressionStatement; // Cached first super-call found in the constructor. Used in checking whether super is called before this-accessing + superCall?: ExpressionStatement; // Cached first super-call found in the constructor. Used in checking whether super is called before this-accessing } export const enum TypeFlags { @@ -2484,7 +2484,7 @@ namespace ts { // When options come from a config file, its path is recorded here configFilePath?: string; /* @internal */ - // Path used to used to compute primary search locations + // Path used to used to compute primary search locations typesRoot?: string; types?: string[]; @@ -2801,7 +2801,7 @@ namespace ts { */ resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; /** - * This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files + * This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; } diff --git a/src/server/server.ts b/src/server/server.ts index 26ab01adc2b7c..ec83f8cc71625 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -111,12 +111,6 @@ namespace ts.server { detailLevel?: string; } - interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; - } - function parseLoggingEnvironmentString(logEnvStr: string): LogOptions { const logEnv: LogOptions = {}; const args = logEnvStr.split(" ");