-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add directory watcher for tsserver and tsc #5127
Changes from all commits
0bc5c14
5daa100
4dcf8c7
98eaeba
7fa26ad
9db53f2
7741ec0
17f0cce
f7e35d5
3b6f283
8171ded
9ed5b4c
002f0c0
def268c
62664fd
6013968
c754999
fcfc25e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -700,6 +700,9 @@ namespace ts { | |
} | ||
|
||
export function getBaseFileName(path: string) { | ||
if (!path) { | ||
return undefined; | ||
} | ||
let i = path.lastIndexOf(directorySeparator); | ||
return i < 0 ? path : path.substring(i + 1); | ||
} | ||
|
@@ -723,6 +726,18 @@ namespace ts { | |
*/ | ||
export const supportedExtensions = [".ts", ".tsx", ".d.ts"]; | ||
|
||
export function isSupportedSourceFileName(fileName: string) { | ||
if (!fileName) { return false; } | ||
|
||
let dotIndex = fileName.lastIndexOf("."); | ||
if (dotIndex < 0) { | ||
return false; | ||
} | ||
|
||
let extension = fileName.slice(dotIndex, fileName.length); | ||
return supportedExtensions.indexOf(extension) >= 0; | ||
} | ||
|
||
const extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"]; | ||
export function removeFileExtension(path: string): string { | ||
for (let ext of extensionsToRemove) { | ||
|
@@ -817,4 +832,14 @@ namespace ts { | |
Debug.assert(false, message); | ||
} | ||
} | ||
} | ||
|
||
export function copyListRemovingItem<T>(item: T, list: T[]) { | ||
let copiedList: T[] = []; | ||
for (var i = 0, len = list.length; i < len; i++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
if (list[i] != item) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
copiedList.push(list[i]); | ||
} | ||
} | ||
return copiedList; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,8 @@ namespace ts { | |
write(s: string): void; | ||
readFile(path: string, encoding?: string): string; | ||
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; | ||
watchFile?(path: string, callback: (path: string, removed: boolean) => void): FileWatcher; | ||
watchFile?(path: string, callback: (path: string, removed?: boolean) => void): FileWatcher; | ||
watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher; | ||
resolvePath(path: string): string; | ||
fileExists(path: string): boolean; | ||
directoryExists(path: string): boolean; | ||
|
@@ -20,6 +21,12 @@ namespace ts { | |
exit(exitCode?: number): void; | ||
} | ||
|
||
interface WatchedFile { | ||
fileName: string; | ||
callback: (fileName: string, removed?: boolean) => void; | ||
mtime: Date; | ||
} | ||
|
||
export interface FileWatcher { | ||
close(): void; | ||
} | ||
|
@@ -192,6 +199,103 @@ namespace ts { | |
const _path = require("path"); | ||
const _os = require("os"); | ||
|
||
// average async stat takes about 30 microseconds | ||
// set chunk size to do 30 files in < 1 millisecond | ||
function createWatchedFileSet(interval = 2500, chunkSize = 30) { | ||
let watchedFiles: WatchedFile[] = []; | ||
let nextFileToCheck = 0; | ||
let watchTimer: any; | ||
|
||
function getModifiedTime(fileName: string): Date { | ||
return _fs.statSync(fileName).mtime; | ||
} | ||
|
||
function poll(checkedIndex: number) { | ||
let watchedFile = watchedFiles[checkedIndex]; | ||
if (!watchedFile) { | ||
return; | ||
} | ||
|
||
_fs.stat(watchedFile.fileName, (err: any, stats: any) => { | ||
if (err) { | ||
watchedFile.callback(watchedFile.fileName); | ||
} | ||
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { | ||
watchedFile.mtime = getModifiedTime(watchedFile.fileName); | ||
watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); | ||
} | ||
}); | ||
} | ||
|
||
// this implementation uses polling and | ||
// stat due to inconsistencies of fs.watch | ||
// and efficiency of stat on modern filesystems | ||
function startWatchTimer() { | ||
watchTimer = setInterval(() => { | ||
let count = 0; | ||
let nextToCheck = nextFileToCheck; | ||
let firstCheck = -1; | ||
while ((count < chunkSize) && (nextToCheck !== firstCheck)) { | ||
poll(nextToCheck); | ||
if (firstCheck < 0) { | ||
firstCheck = nextToCheck; | ||
} | ||
nextToCheck++; | ||
if (nextToCheck === watchedFiles.length) { | ||
nextToCheck = 0; | ||
} | ||
count++; | ||
} | ||
nextFileToCheck = nextToCheck; | ||
}, interval); | ||
} | ||
|
||
function addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile { | ||
let file: WatchedFile = { | ||
fileName, | ||
callback, | ||
mtime: getModifiedTime(fileName) | ||
}; | ||
|
||
watchedFiles.push(file); | ||
if (watchedFiles.length === 1) { | ||
startWatchTimer(); | ||
} | ||
return file; | ||
} | ||
|
||
function removeFile(file: WatchedFile) { | ||
watchedFiles = copyListRemovingItem(file, watchedFiles); | ||
} | ||
|
||
return { | ||
getModifiedTime: getModifiedTime, | ||
poll: poll, | ||
startWatchTimer: startWatchTimer, | ||
addFile: addFile, | ||
removeFile: removeFile | ||
}; | ||
} | ||
|
||
// REVIEW: for now this implementation uses polling. | ||
// The advantage of polling is that it works reliably | ||
// on all os and with network mounted files. | ||
// For 90 referenced files, the average time to detect | ||
// changes is 2*msInterval (by default 5 seconds). | ||
// The overhead of this is .04 percent (1/2500) with | ||
// average pause of < 1 millisecond (and max | ||
// pause less than 1.5 milliseconds); question is | ||
// do we anticipate reference sets in the 100s and | ||
// do we care about waiting 10-20 seconds to detect | ||
// changes for large reference sets? If so, do we want | ||
// to increase the chunk size or decrease the interval | ||
// time dynamically to match the large reference set? | ||
let watchedFileSet = createWatchedFileSet(); | ||
|
||
function isNode4OrLater(): Boolean { | ||
return parseInt(process.version.charAt(1)) >= 4; | ||
} | ||
|
||
const platform: string = _os.platform(); | ||
// win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive | ||
const useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin"; | ||
|
@@ -284,25 +388,36 @@ namespace ts { | |
readFile, | ||
writeFile, | ||
watchFile: (fileName, callback) => { | ||
// watchFile polls a file every 250ms, picking up file notifications. | ||
_fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); | ||
// Node 4.0 stablized the `fs.watch` function on Windows which avoids polling | ||
// and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649 | ||
// and https://github.com/Microsoft/TypeScript/issues/4643), therefore | ||
// if the current node.js version is newer than 4, use `fs.watch` instead. | ||
if (isNode4OrLater()) { | ||
// Note: in node the callback of fs.watch is given only the relative file name as a parameter | ||
return _fs.watch(fileName, (eventName: string, relativeFileName: string) => callback(fileName)); | ||
} | ||
|
||
let watchedFile = watchedFileSet.addFile(fileName, callback); | ||
return { | ||
close() { _fs.unwatchFile(fileName, fileChanged); } | ||
close: () => watchedFileSet.removeFile(watchedFile) | ||
}; | ||
|
||
function fileChanged(curr: any, prev: any) { | ||
// mtime.getTime() equals 0 if file was removed | ||
if (curr.mtime.getTime() === 0) { | ||
callback(fileName, /* removed */ true); | ||
return; | ||
} | ||
if (+curr.mtime <= +prev.mtime) { | ||
return; | ||
}, | ||
watchDirectory: (path, 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 _fs.watch( | ||
path, | ||
{ persisten: true, recursive: !!recursive }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
(eventName: string, relativeFileName: string) => { | ||
// 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(ts.combinePaths(path, relativeFileName))); | ||
}; | ||
} | ||
|
||
callback(fileName, /* removed */ false); | ||
} | ||
); | ||
}, | ||
resolvePath: function (path: string): string { | ||
return _path.resolve(path); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.