Skip to content

Commit 8864b06

Browse files
committed
Merge pull request #5127 from zhengbli/newAddDirectoryWatcher
Add directory watcher for tsserver and tsc
2 parents 2bf39a6 + fcfc25e commit 8864b06

File tree

12 files changed

+471
-223
lines changed

12 files changed

+471
-223
lines changed

src/compiler/commandLineParser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,15 +384,15 @@ namespace ts {
384384
catch (e) {
385385
return { error: createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) };
386386
}
387-
return parseConfigFileText(fileName, text);
387+
return parseConfigFileTextToJson(fileName, text);
388388
}
389389

390390
/**
391391
* Parse the text of the tsconfig.json file
392392
* @param fileName The path to the config file
393393
* @param jsonText The text of the config file
394394
*/
395-
export function parseConfigFileText(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } {
395+
export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } {
396396
try {
397397
return { config: /\S/.test(jsonText) ? JSON.parse(jsonText) : {} };
398398
}
@@ -407,7 +407,7 @@ namespace ts {
407407
* @param basePath A root directory to resolve relative path entries in the config
408408
* file to. e.g. outDir
409409
*/
410-
export function parseConfigFile(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine {
410+
export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine {
411411
let errors: Diagnostic[] = [];
412412

413413
return {

src/compiler/core.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,9 @@ namespace ts {
704704
}
705705

706706
export function getBaseFileName(path: string) {
707+
if (!path) {
708+
return undefined;
709+
}
707710
let i = path.lastIndexOf(directorySeparator);
708711
return i < 0 ? path : path.substring(i + 1);
709712
}
@@ -733,6 +736,18 @@ namespace ts {
733736
*/
734737
export const moduleFileExtensions = supportedExtensions;
735738

739+
export function isSupportedSourceFileName(fileName: string) {
740+
if (!fileName) { return false; }
741+
742+
let dotIndex = fileName.lastIndexOf(".");
743+
if (dotIndex < 0) {
744+
return false;
745+
}
746+
747+
let extension = fileName.slice(dotIndex, fileName.length);
748+
return supportedExtensions.indexOf(extension) >= 0;
749+
}
750+
736751
const extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"];
737752
export function removeFileExtension(path: string): string {
738753
for (let ext of extensionsToRemove) {
@@ -827,4 +842,14 @@ namespace ts {
827842
Debug.assert(false, message);
828843
}
829844
}
830-
}
845+
846+
export function copyListRemovingItem<T>(item: T, list: T[]) {
847+
let copiedList: T[] = [];
848+
for (var i = 0, len = list.length; i < len; i++) {
849+
if (list[i] != item) {
850+
copiedList.push(list[i]);
851+
}
852+
}
853+
return copiedList;
854+
}
855+
}

src/compiler/sys.ts

Lines changed: 131 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ namespace ts {
88
write(s: string): void;
99
readFile(path: string, encoding?: string): string;
1010
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
11-
watchFile?(path: string, callback: (path: string, removed: boolean) => void): FileWatcher;
11+
watchFile?(path: string, callback: (path: string, removed?: boolean) => void): FileWatcher;
12+
watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher;
1213
resolvePath(path: string): string;
1314
fileExists(path: string): boolean;
1415
directoryExists(path: string): boolean;
@@ -20,6 +21,12 @@ namespace ts {
2021
exit(exitCode?: number): void;
2122
}
2223

24+
interface WatchedFile {
25+
fileName: string;
26+
callback: (fileName: string, removed?: boolean) => void;
27+
mtime: Date;
28+
}
29+
2330
export interface FileWatcher {
2431
close(): void;
2532
}
@@ -192,6 +199,103 @@ namespace ts {
192199
const _path = require("path");
193200
const _os = require("os");
194201

202+
// average async stat takes about 30 microseconds
203+
// set chunk size to do 30 files in < 1 millisecond
204+
function createWatchedFileSet(interval = 2500, chunkSize = 30) {
205+
let watchedFiles: WatchedFile[] = [];
206+
let nextFileToCheck = 0;
207+
let watchTimer: any;
208+
209+
function getModifiedTime(fileName: string): Date {
210+
return _fs.statSync(fileName).mtime;
211+
}
212+
213+
function poll(checkedIndex: number) {
214+
let watchedFile = watchedFiles[checkedIndex];
215+
if (!watchedFile) {
216+
return;
217+
}
218+
219+
_fs.stat(watchedFile.fileName, (err: any, stats: any) => {
220+
if (err) {
221+
watchedFile.callback(watchedFile.fileName);
222+
}
223+
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) {
224+
watchedFile.mtime = getModifiedTime(watchedFile.fileName);
225+
watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0);
226+
}
227+
});
228+
}
229+
230+
// this implementation uses polling and
231+
// stat due to inconsistencies of fs.watch
232+
// and efficiency of stat on modern filesystems
233+
function startWatchTimer() {
234+
watchTimer = setInterval(() => {
235+
let count = 0;
236+
let nextToCheck = nextFileToCheck;
237+
let firstCheck = -1;
238+
while ((count < chunkSize) && (nextToCheck !== firstCheck)) {
239+
poll(nextToCheck);
240+
if (firstCheck < 0) {
241+
firstCheck = nextToCheck;
242+
}
243+
nextToCheck++;
244+
if (nextToCheck === watchedFiles.length) {
245+
nextToCheck = 0;
246+
}
247+
count++;
248+
}
249+
nextFileToCheck = nextToCheck;
250+
}, interval);
251+
}
252+
253+
function addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile {
254+
let file: WatchedFile = {
255+
fileName,
256+
callback,
257+
mtime: getModifiedTime(fileName)
258+
};
259+
260+
watchedFiles.push(file);
261+
if (watchedFiles.length === 1) {
262+
startWatchTimer();
263+
}
264+
return file;
265+
}
266+
267+
function removeFile(file: WatchedFile) {
268+
watchedFiles = copyListRemovingItem(file, watchedFiles);
269+
}
270+
271+
return {
272+
getModifiedTime: getModifiedTime,
273+
poll: poll,
274+
startWatchTimer: startWatchTimer,
275+
addFile: addFile,
276+
removeFile: removeFile
277+
};
278+
}
279+
280+
// REVIEW: for now this implementation uses polling.
281+
// The advantage of polling is that it works reliably
282+
// on all os and with network mounted files.
283+
// For 90 referenced files, the average time to detect
284+
// changes is 2*msInterval (by default 5 seconds).
285+
// The overhead of this is .04 percent (1/2500) with
286+
// average pause of < 1 millisecond (and max
287+
// pause less than 1.5 milliseconds); question is
288+
// do we anticipate reference sets in the 100s and
289+
// do we care about waiting 10-20 seconds to detect
290+
// changes for large reference sets? If so, do we want
291+
// to increase the chunk size or decrease the interval
292+
// time dynamically to match the large reference set?
293+
let watchedFileSet = createWatchedFileSet();
294+
295+
function isNode4OrLater(): Boolean {
296+
return parseInt(process.version.charAt(1)) >= 4;
297+
}
298+
195299
const platform: string = _os.platform();
196300
// win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive
197301
const useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin";
@@ -284,25 +388,36 @@ namespace ts {
284388
readFile,
285389
writeFile,
286390
watchFile: (fileName, callback) => {
287-
// watchFile polls a file every 250ms, picking up file notifications.
288-
_fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged);
391+
// Node 4.0 stablized the `fs.watch` function on Windows which avoids polling
392+
// and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649
393+
// and https://github.com/Microsoft/TypeScript/issues/4643), therefore
394+
// if the current node.js version is newer than 4, use `fs.watch` instead.
395+
if (isNode4OrLater()) {
396+
// Note: in node the callback of fs.watch is given only the relative file name as a parameter
397+
return _fs.watch(fileName, (eventName: string, relativeFileName: string) => callback(fileName));
398+
}
289399

400+
let watchedFile = watchedFileSet.addFile(fileName, callback);
290401
return {
291-
close() { _fs.unwatchFile(fileName, fileChanged); }
402+
close: () => watchedFileSet.removeFile(watchedFile)
292403
};
293-
294-
function fileChanged(curr: any, prev: any) {
295-
// mtime.getTime() equals 0 if file was removed
296-
if (curr.mtime.getTime() === 0) {
297-
callback(fileName, /* removed */ true);
298-
return;
299-
}
300-
if (+curr.mtime <= +prev.mtime) {
301-
return;
404+
},
405+
watchDirectory: (path, callback, recursive) => {
406+
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
407+
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
408+
return _fs.watch(
409+
path,
410+
{ persisten: true, recursive: !!recursive },
411+
(eventName: string, relativeFileName: string) => {
412+
// In watchDirectory we only care about adding and removing files (when event name is
413+
// "rename"); changes made within files are handled by corresponding fileWatchers (when
414+
// event name is "change")
415+
if (eventName === "rename") {
416+
// When deleting a file, the passed baseFileName is null
417+
callback(!relativeFileName ? relativeFileName : normalizePath(ts.combinePaths(path, relativeFileName)));
418+
};
302419
}
303-
304-
callback(fileName, /* removed */ false);
305-
}
420+
);
306421
},
307422
resolvePath: function (path: string): string {
308423
return _path.resolve(path);

0 commit comments

Comments
 (0)