diff --git a/Gulpfile.ts b/Gulpfile.ts index aedde5c33d955..d2f208f6207b2 100644 --- a/Gulpfile.ts +++ b/Gulpfile.ts @@ -577,7 +577,7 @@ gulp.task(specMd, /*help*/ false, [word2mdJs], (done) => { gulp.task("generate-spec", "Generates a Markdown version of the Language Specification", [specMd]); gulp.task("clean", "Cleans the compiler output, declare files, and tests", [], () => { - return del([builtDirectory]); + return del([builtDirectory, processDiagnosticMessagesJs, generatedDiagnosticMessagesJSON, diagnosticInfoMapTs]); }); gulp.task("useDebugMode", /*help*/ false, [], (done) => { useDebugMode = true; done(); }); diff --git a/Jakefile.js b/Jakefile.js index 4f39d6af5c6cf..c9cdb6b3abd6e 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -716,6 +716,9 @@ task("default", ["local"]); desc("Cleans the compiler output, declare files, and tests"); task("clean", function () { jake.rmRf(builtDirectory); + jake.rmRf(processDiagnosticMessagesJs); + jake.rmRf(generatedDiagnosticMessagesJSON); + jake.rmRf(diagnosticInfoMapTs); }); // Generate Markdown spec diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 989bb6a7142d2..597e017d4821c 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -2025,7 +2025,7 @@ namespace ts { export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { basePath = normalizePath(basePath); - const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; + const keyMapper = host.useCaseSensitiveFileNames ? identity : toLowerCase; // Literal file names (provided via the "files" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map later when when including @@ -2232,24 +2232,6 @@ namespace ts { } } - /** - * Gets a case sensitive key. - * - * @param key The original key. - */ - function caseSensitiveKeyMapper(key: string) { - return key; - } - - /** - * Gets a case insensitive key. - * - * @param key The original key. - */ - function caseInsensitiveKeyMapper(key: string) { - return key.toLowerCase(); - } - /** * Produces a cleaned version of compiler options with personally identifiying info (aka, paths) removed. * Also converts enum values back to strings. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index fdf888b0cfcb0..a7c277709e5b6 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -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(): MapLike { const map = Object.create(/*prototype*/ null); // tslint:disable-line:no-null-keyword @@ -832,6 +838,44 @@ namespace ts { return result; } + /** + * Enumreates on two sorted arrays newItems and oldItems by invoking + * - inserted on items that are present in newItems but not in oldItems + * - deleted on items that are present in oldItems but not in newIterms + * - unchanged if provided on items that are present in both newItems and oldItems + */ + export function enumerateInsertsAndDeletes(newItems: ReadonlyArray, oldItems: ReadonlyArray, 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++]); + } + } + export function sum, K extends string>(array: ReadonlyArray, prop: K): number { let result = 0; for (const v of array) { @@ -1398,6 +1442,9 @@ namespace ts { /** Returns its argument. */ export function identity(x: T) { return x; } + /** Returns the lower case string */ + export function toLowerCase(x: string) { return x.toLowerCase(); } + /** Throws an error because a function is not implemented. */ export function notImplemented(): never { throw new Error("Not implemented"); @@ -2871,9 +2918,7 @@ namespace ts { export type GetCanonicalFileName = (fileName: string) => string; export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { - return useCaseSensitiveFileNames - ? ((fileName) => fileName) - : ((fileName) => fileName.toLowerCase()); + return useCaseSensitiveFileNames ? identity : toLowerCase; } /** diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index aa928cdaacdcb..cec06ac669497 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -124,91 +124,233 @@ namespace ts { getEnvironmentVariable?(name: string): string; }; - export let sys: System = (() => { - const utf8ByteOrderMark = "\u00EF\u00BB\u00BF"; - - function getNodeSystem(): System { - const _fs = require("fs"); - const _path = require("path"); - const _os = require("os"); - const _crypto = require("crypto"); + /*@internal*/ + export interface PollingWatchDirectoryHost { + watchFile: System["watchFile"]; + getAccessileSortedChildDirectories(path: string): ReadonlyArray; + directoryExists(path: string): boolean; + filePathComparer: Comparer; + } - const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; + /** + * Watch the directory using polling watchFile + * 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 watchDirectoryWithPolling(directoryName: string, recursive: boolean | undefined, host: PollingWatchDirectoryHost, + onChangedPollingDirectory: () => void, onDeletedPollingDirectory?: () => void): FileWatcher { + type ChildWatches = ReadonlyArray; + interface DirectoryWatcher extends FileWatcher { + childWatches: ChildWatches; + } + interface ChildDirectoryWatcher extends DirectoryWatcher { + childName: string; + } - function createWatchedFileSet() { - const dirWatchers = createMap(); - // One file can have multiple watchers - const fileWatcherCallbacks = createMultiMap(); - return { addFile, removeFile }; + return createDirectoryWatcher(directoryName); - function reduceDirWatcherRefCountForFile(fileName: string) { - const dirName = getDirectoryPath(fileName); - const watcher = dirWatchers.get(dirName); - if (watcher) { - watcher.referenceCount -= 1; - if (watcher.referenceCount <= 0) { - watcher.close(); - dirWatchers.delete(dirName); - } - } - } - - function addDirWatcher(dirPath: string): void { - let watcher = dirWatchers.get(dirPath); - if (watcher) { - watcher.referenceCount += 1; + /** + * Create the directory watcher for the dirPath. + * If not watching recursively just return FileWatcher, + * otherwise create DirectoryWatcher that watches child directories as well + */ + function createDirectoryWatcher(dirPath: string): DirectoryWatcher | FileWatcher { + const dirWatcher = host.watchFile(dirPath, (_fileName, eventKind) => { + if (dirPath === directoryName) { + if (onDeletedPollingDirectory && eventKind === FileWatcherEventKind.Deleted) { + // Watch missing directory hence forward + onDeletedPollingDirectory(); return; } - watcher = fsWatchDirectory( - dirPath || ".", - (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath) - ) as DirectoryWatcher; - watcher.referenceCount = 1; - dirWatchers.set(dirPath, watcher); + } + // Create and delete should be handled by parent, no special action needed + else if (eventKind !== FileWatcherEventKind.Changed) { return; } - function addFileWatcherCallback(filePath: string, callback: FileWatcherCallback): void { - fileWatcherCallbacks.add(filePath, callback); + // For now just call the rename on current directory + onChangedPollingDirectory(); + + // Iterate through existing children and update the watches if needed + if (result) { + result.childWatches = watchChildDirectoriesWithPolling(dirPath, result.childWatches); } + }); - function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { - addFileWatcherCallback(fileName, callback); - addDirWatcher(getDirectoryPath(fileName)); + // If not recursive just use this watcher, no need to iterate through children + if (!recursive) { + return dirWatcher; + } - return { fileName, callback }; - } + let result: DirectoryWatcher = { + close: () => { + dirWatcher.close(); + result.childWatches.forEach(closeFileWatcher); + result = undefined; + }, + childWatches: watchChildDirectoriesWithPolling(dirPath, emptyArray) + }; + return result; + } - function removeFile(watchedFile: WatchedFile) { - removeFileWatcherCallback(watchedFile.fileName, watchedFile.callback); - reduceDirWatcherRefCountForFile(watchedFile.fileName); - } + /** + * Watch the directories in the parentDir + */ + function watchChildDirectoriesWithPolling(parentDir: string, existingChildWatches: ChildWatches): ChildWatches { + if (!host.directoryExists(parentDir)) { + return emptyArray; + } - function removeFileWatcherCallback(filePath: string, callback: FileWatcherCallback) { - fileWatcherCallbacks.remove(filePath, callback); - } + let newChildWatches: ChildDirectoryWatcher[] | undefined; + enumerateInsertsAndDeletes( + host.getAccessileSortedChildDirectories(parentDir), + existingChildWatches, + (child, childWatcher) => host.filePathComparer(child, childWatcher.childName), + createAndAddChildDirectoryWatcher, + closeFileWatcher, + addChildDirectoryWatcher + ); + return newChildWatches || emptyArray; + + /** + * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list + */ + function createAndAddChildDirectoryWatcher(childName: string) { + const childPath = ts.getNormalizedAbsolutePath(childName, parentDir); + const result = createDirectoryWatcher(childPath) as ChildDirectoryWatcher; + result.childName = childName; + addChildDirectoryWatcher(result); + } - function fileEventHandler(eventName: string, relativeFileName: string | undefined, baseDirPath: string) { - // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - const fileName = !isString(relativeFileName) - ? undefined - : ts.getNormalizedAbsolutePath(relativeFileName, baseDirPath); - // Some applications save a working file via rename operations - if ((eventName === "change" || eventName === "rename")) { - const callbacks = fileWatcherCallbacks.get(fileName); - if (callbacks) { - for (const fileCallback of callbacks) { - fileCallback(fileName, FileWatcherEventKind.Changed); - } - } - } - } + /** + * Add child directory watcher to the new ChildDirectoryWatcher list + */ + function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) { + (newChildWatches || (newChildWatches = [])).push(childWatcher); } - const watchedFileSet = createWatchedFileSet(); + } + } + + export let sys: System = (() => { + const utf8ByteOrderMark = "\u00EF\u00BB\u00BF"; + + function getNodeSystem(): System { + const _fs = require("fs"); + const _path = require("path"); + const _os = require("os"); + const _crypto = require("crypto"); const nodeVersion = getNodeMajorVersion(); const isNode4OrLater = nodeVersion >= 4; + const platform: string = _os.platform(); + const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + + const enum FileSystemEntryKind { + File, + Directory + } + + const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; + // 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 fsSupportsRecursiveWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); + const filePathComparer = useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive; + let pollingWatchPresentDirectoryHost: PollingWatchDirectoryHost | undefined; + let pollingWatchPresentOrMissingDirectoryHost: PollingWatchDirectoryHost | undefined; + + const nodeSystem: System = { + args: process.argv.slice(2), + newLine: _os.EOL, + useCaseSensitiveFileNames, + write(s: string): void { + process.stdout.write(s); + }, + readFile, + writeFile, + watchFile: useNonPollingWatchers ? createNonPollingWatchFile() : fsWatchFile, + 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); + }, + resolvePath: path => _path.resolve(path), + fileExists, + directoryExists, + createDirectory(directoryName: string) { + if (!nodeSystem.directoryExists(directoryName)) { + _fs.mkdirSync(directoryName); + } + }, + getExecutingFilePath() { + return __filename; + }, + getCurrentDirectory() { + return process.cwd(); + }, + getDirectories, + getEnvironmentVariable(name: string) { + return process.env[name] || ""; + }, + readDirectory, + getModifiedTime(path) { + try { + return _fs.statSync(path).mtime; + } + catch (e) { + return undefined; + } + }, + createHash(data) { + const hash = _crypto.createHash("md5"); + hash.update(data); + return hash.digest("hex"); + }, + getMemoryUsage() { + if (global.gc) { + global.gc(); + } + return process.memoryUsage().heapUsed; + }, + getFileSize(path) { + try { + const stat = _fs.statSync(path); + if (stat.isFile()) { + return stat.size; + } + } + catch { /*ignore*/ } + return 0; + }, + exit(exitCode?: number): void { + process.exit(exitCode); + }, + realpath(path: string): string { + return _fs.realpathSync(path); + }, + debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), + tryEnableSourceMapsForHost() { + try { + require("source-map-support").install(); + } + catch { + // Could not enable source maps. + } + }, + setTimeout, + clearTimeout + }; + return nodeSystem; + function isFileSystemCaseSensitive(): boolean { // win32\win64 are case insensitive platforms if (platform === "win32" || platform === "win64") { @@ -226,93 +368,180 @@ namespace ts { }); } - const platform: string = _os.platform(); - const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + function createNonPollingWatchFile() { + // One file can have multiple watchers + const fileWatcherCallbacks = createMultiMap(); + const dirWatchers = createMap(); + const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return nonPollingWatchFile; + + function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { + const filePath = toCanonicalName(fileName); + fileWatcherCallbacks.add(filePath, callback); + const dirPath = getDirectoryPath(filePath) || "."; + const watcher = dirWatchers.get(dirPath) || createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath); + watcher.referenceCount++; + return { + close: () => { + if (watcher.referenceCount === 1) { + watcher.close(); + dirWatchers.delete(dirPath); + } + else { + watcher.referenceCount--; + } + fileWatcherCallbacks.remove(filePath, callback); + } + }; + } + + function createDirectoryWatcher(dirName: string, dirPath: string) { + const watcher = fsWatchDirectory( + dirName, + (_eventName: string, relativeFileName) => { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + const fileName = !isString(relativeFileName) + ? undefined + : ts.getNormalizedAbsolutePath(relativeFileName, dirName); + // Some applications save a working file via rename operations + const callbacks = fileWatcherCallbacks.get(toCanonicalName(fileName)); + if (callbacks) { + for (const fileCallback of callbacks) { + fileCallback(fileName, FileWatcherEventKind.Changed); + } + } + } + ) as DirectoryWatcher; + watcher.referenceCount = 0; + dirWatchers.set(dirPath, watcher); + return watcher; + } + } function fsWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); + let eventKind: FileWatcherEventKind; return { close: () => _fs.unwatchFile(fileName, fileChanged) }; function fileChanged(curr: any, prev: any) { - const isCurrZero = +curr.mtime === 0; - const isPrevZero = +prev.mtime === 0; - const created = !isCurrZero && isPrevZero; - const deleted = isCurrZero && !isPrevZero; - - const eventKind = created - ? FileWatcherEventKind.Created - : deleted - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; - - if (eventKind === FileWatcherEventKind.Changed && +curr.mtime <= +prev.mtime) { + if (+curr.mtime === 0) { + eventKind = FileWatcherEventKind.Deleted; + } + // previous event kind check is to ensure we send created event when file is restored or renamed twice (that is it disappears and reappears) + // since in that case the prevTime returned is same as prev time of event when file was deleted as per node documentation + else if (+prev.mtime === 0 || eventKind === FileWatcherEventKind.Deleted) { + eventKind = FileWatcherEventKind.Created; + } + // If there is no change in modified time, ignore the event + else if (+curr.mtime === +prev.mtime) { return; } - + else { + // File changed + eventKind = FileWatcherEventKind.Changed; + } callback(fileName, eventKind); } } function fsWatchDirectory(directoryName: string, callback: (eventName: string, relativeFileName: string) => void, recursive?: boolean): FileWatcher { - let options: any; - /** Watcher for the directory depending on whether it is missing or present */ - let watcher = !directoryExists(directoryName) ? - watchMissingDirectory() : - watchPresentDirectory(); + // When doing recursive watch on non supported system, just use polling watcher + if (recursive && !fsSupportsRecursiveWatch) { + return watchPresentOrMissingDirectoryWithPolling(); + } + + /** + * Watcher for the directory depending on whether it is missing or present + */ + let watcher = !directoryExists(directoryName) ? watchMissingDirectoryWithPolling() : fsWatchPresentDirectory(); return { close: () => { // Close the watcher (either existing directory watcher or missing directory watcher) watcher.close(); + watcher = undefined; } }; + function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher) { + // Call the callback for current directory + callback("rename", ""); + + // If watcher is not closed, update it + if (watcher) { + watcher.close(); + watcher = createWatcher(); + } + } + /** - * Watch the directory that is currently present - * and when the watched directory is deleted, switch to missing directory watcher + * Watch the present directory through fs.watch and if that results in exception use polling directory watching + * when directory goes missing use switch to missing directory watcher */ - function watchPresentDirectory(): FileWatcher { - // 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) - if (options === undefined) { - if (isNode4OrLater && (process.platform === "win32" || process.platform === "darwin")) { - options = { persistent: true, recursive: !!recursive }; - } - else { - options = { persistent: true }; - } + function fsWatchPresentDirectory(): FileWatcher { + try { + const dirWatcher = _fs.watch( + directoryName, + { persistent: true, recursive: !!recursive }, + callback + ); + // Watch the missing directory on error (eg. directory deleted) + dirWatcher.on("error", () => invokeCallbackAndUpdateWatcher(watchMissingDirectoryWithPolling)); + return dirWatcher; + } + catch (e) { + // Catch the exception and use polling instead + // 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 + // so instead of throwing error, use polling directory watcher + return watchPresentDirectoryWithPolling(); } + } - const dirWatcher = _fs.watch( - directoryName, - options, - callback - ); - dirWatcher.on("error", () => { - if (!directoryExists(directoryName)) { - // Deleting directory - watcher = watchMissingDirectory(); - // Call the callback for current directory - callback("rename", ""); - } - }); - return dirWatcher; + /** + * Watch the existing directory using polling, + * that means if this is recursive watcher, watch the children directories as well + * (eg on OS that dont support recursive watch using fs.watch) + */ + function watchPresentDirectoryWithPolling(): FileWatcher { + return watchDirectoryWithPolling(directoryName, recursive, + pollingWatchPresentDirectoryHost || (pollingWatchPresentDirectoryHost = { + filePathComparer, + watchFile: fsWatchFile, + // Since we are watching present directory, it would always be present + directoryExists: returnTrue, + getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories + }), + () => callback("rename", ""), + () => invokeCallbackAndUpdateWatcher(watchMissingDirectoryWithPolling)); + } + + /** + * Watch missing or present directory with polling, + * this is invoked when recursive is not supported through fs.watch + * which means we would always need to poll, so no need to handle missing directory separately + */ + function watchPresentOrMissingDirectoryWithPolling(): FileWatcher { + return watchDirectoryWithPolling(directoryName, recursive, + pollingWatchPresentOrMissingDirectoryHost || (pollingWatchPresentOrMissingDirectoryHost = { + filePathComparer, + watchFile: fsWatchFile, + directoryExists, + getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories + }), + () => callback("rename", "")); } /** * Watch the directory that is missing * and switch to existing directory when the directory is created */ - function watchMissingDirectory(): FileWatcher { + function watchMissingDirectoryWithPolling(): FileWatcher { return fsWatchFile(directoryName, (_fileName, eventKind) => { if (eventKind === FileWatcherEventKind.Created && directoryExists(directoryName)) { - watcher.close(); - watcher = watchPresentDirectory(); - // Call the callback for current directory - // For now it could be callback for the inner directory creation, - // but just return current directory, better than current no-op - callback("rename", ""); + // This could be resulted as part of creating another directory or file + // but instead of spending time to detect that invoke callback on current directory + invokeCallbackAndUpdateWatcher(fsWatchPresentDirectory); } }); } @@ -368,7 +597,7 @@ namespace ts { function getAccessibleFileSystemEntries(path: string): FileSystemEntries { try { - const entries = _fs.readdirSync(path || ".").sort(); + const entries = _fs.readdirSync(path || ".").sort(filePathComparer); const files: string[] = []; const directories: string[] = []; for (const entry of entries) { @@ -405,11 +634,6 @@ namespace ts { return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries); } - const enum FileSystemEntryKind { - File, - Directory - } - function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { try { const stat = _fs.statSync(path); @@ -434,107 +658,6 @@ namespace ts { function getDirectories(path: string): string[] { return filter(_fs.readdirSync(path), dir => fileSystemEntryExists(combinePaths(path, dir), FileSystemEntryKind.Directory)); } - - const nodeSystem: System = { - args: process.argv.slice(2), - newLine: _os.EOL, - useCaseSensitiveFileNames, - write(s: string): void { - process.stdout.write(s); - }, - readFile, - writeFile, - watchFile: (fileName, callback, pollingInterval) => { - if (useNonPollingWatchers) { - const watchedFile = watchedFileSet.addFile(fileName, callback); - return { - close: () => watchedFileSet.removeFile(watchedFile) - }; - } - else { - return fsWatchFile(fileName, callback, pollingInterval); - } - }, - 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); - }, - resolvePath: path => _path.resolve(path), - fileExists, - directoryExists, - createDirectory(directoryName: string) { - if (!nodeSystem.directoryExists(directoryName)) { - _fs.mkdirSync(directoryName); - } - }, - getExecutingFilePath() { - return __filename; - }, - getCurrentDirectory() { - return process.cwd(); - }, - getDirectories, - getEnvironmentVariable(name: string) { - return process.env[name] || ""; - }, - readDirectory, - getModifiedTime(path) { - try { - return _fs.statSync(path).mtime; - } - catch (e) { - return undefined; - } - }, - createHash(data) { - const hash = _crypto.createHash("md5"); - hash.update(data); - return hash.digest("hex"); - }, - getMemoryUsage() { - if (global.gc) { - global.gc(); - } - return process.memoryUsage().heapUsed; - }, - getFileSize(path) { - try { - const stat = _fs.statSync(path); - if (stat.isFile()) { - return stat.size; - } - } - catch { /*ignore*/ } - return 0; - }, - exit(exitCode?: number): void { - process.exit(exitCode); - }, - realpath(path: string): string { - return _fs.realpathSync(path); - }, - debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), - tryEnableSourceMapsForHost() { - try { - require("source-map-support").install(); - } - catch { - // Could not enable source maps. - } - }, - setTimeout, - clearTimeout - }; - return nodeSystem; } function getChakraSystem(): System { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 28520919e1eff..7704d5581278d 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2,7 +2,6 @@ /* @internal */ namespace ts { - export const emptyArray: never[] = [] as never[]; export const emptyMap: ReadonlyMap = createMap(); export const externalHelpersModuleNameText = "tslib"; diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 0cf38f372d907..7dbe208d147b4 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -151,10 +151,6 @@ namespace ts { }; } - export function closeFileWatcher(watcher: FileWatcher) { - watcher.close(); - } - export function closeFileWatcherOf(objWithWatcher: T) { objWithWatcher.watcher.close(); } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 4e2d63cec90ea..00e51cc6da3f3 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -7,7 +7,7 @@ namespace ts.tscWatch { import WatchedSystem = ts.TestFSWithWatch.TestServerHost; type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; import createWatchedSystem = ts.TestFSWithWatch.createWatchedSystem; - import checkFileNames = ts.TestFSWithWatch.checkFileNames; + import checkArray = ts.TestFSWithWatch.checkArray; import libFile = ts.TestFSWithWatch.libFile; import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; @@ -15,11 +15,11 @@ namespace ts.tscWatch { import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain; export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { - checkFileNames(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); } export function checkProgramRootFiles(program: Program, expectedFiles: string[]) { - checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); } function createWatchingSystemHost(system: WatchedSystem) { @@ -2027,4 +2027,50 @@ declare module "fs" { assert.equal(host.readFile(outputFile1), file1.content + host.newLine); }); }); + + describe("tsc-watch when watchingDirectories with polling", () => { + it("when renaming file in subfolder", () => { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: FileOrFolder = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const file: FileOrFolder = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + const programFiles = [file, libFile]; + const files = [file, configFile, libFile]; + const host = createWatchedSystem(files); + host.setWatchDirectoryWithPolling(); + const watch = createWatchModeWithConfigFile(configFile.path, host); + // Watching config file, file, lib file and directories + const expectedWatchedFiles = files.map(f => f.path).concat(projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`); + + verifyProgram(/*isInitial*/ true); + + // Rename the file: + file.path = file.path.replace("file1.ts", "file2.ts"); + expectedWatchedFiles[0] = file.path; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProgram(); + + function verifyProgram(isInitial?: true) { + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + // Watching config file, file, lib file and directories + checkWatchedFiles(host, expectedWatchedFiles); + + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray, isInitial); + + const outputFile = changeExtension(file.path, ".js"); + assert.isTrue(host.fileExists(outputFile)); + assert.equal(host.readFile(outputFile), file.content); + } + }); + }); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index caeb3a8196314..4f2bc1846a9a5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -10,7 +10,7 @@ namespace ts.projectSystem { export import TestServerHost = ts.TestFSWithWatch.TestServerHost; export type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; export import createServerHost = ts.TestFSWithWatch.createServerHost; - export import checkFileNames = ts.TestFSWithWatch.checkFileNames; + export import checkArray = ts.TestFSWithWatch.checkArray; export import libFile = ts.TestFSWithWatch.libFile; export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; @@ -330,11 +330,11 @@ namespace ts.projectSystem { } export function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); + checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); } function checkProjectRootFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); + checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { @@ -367,7 +367,7 @@ namespace ts.projectSystem { } function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) { - checkFileNames("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path)); + checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path)); } /** @@ -506,7 +506,7 @@ namespace ts.projectSystem { const project = projectService.inferredProjects[0]; - checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); + checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); @@ -6470,4 +6470,62 @@ namespace ts.projectSystem { verifyWatchedDirectories(/*useProjectAtRoot*/ false); }); }); + + describe("WatchingDirectories with polling", () => { + it("when file is added to subfolder, completion list has new file", () => { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: FileOrFolder = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const index: FileOrFolder = { + path: `${projectSrcFolder}/index.ts`, + content: `import {} from "./"` + }; + const file1: FileOrFolder = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + + const files = [index, file1, configFile, libFile]; + const fileNames = files.map(file => file.path); + // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder + const expectedWatchedFiles = fileNames.slice(1).concat(projectFolder, projectSrcFolder, `${projectFolder}/${nodeModulesAtTypes}`); + const expectedCompletions = ["file1"]; + const completionPosition = index.content.lastIndexOf('"'); + const host = createServerHost(files); + host.setWatchDirectoryWithPolling(); + const projectService = createProjectService(host); + projectService.openClientFile(index.path); + + const project = projectService.configuredProjects.get(configFile.path); + assert.isDefined(project); + verifyProjectAndCompletions(); + + // Add file2 + const file2: FileOrFolder = { + path: `${projectSrcFolder}/file2.ts`, + content: "" + }; + files.push(file2); + fileNames.push(file2.path); + expectedWatchedFiles.push(file2.path); + expectedCompletions.push("file2"); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + verifyProjectAndCompletions(); + + function verifyProjectAndCompletions() { + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkWatchedFiles(host, expectedWatchedFiles); + checkProjectActualFiles(project, fileNames); + + const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false }); + checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); + } + }); + }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index d4d203cabbbe1..f74370bfef9c3 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -143,10 +143,10 @@ interface Array {}` } } - export function checkFileNames(caption: string, actualFileNames: ReadonlyArray, expectedFileNames: string[]) { - assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected:\r\n${expectedFileNames.join("\r\n")}\r\ngot: ${actualFileNames.join("\r\n")}`); - for (const f of expectedFileNames) { - assert.equal(true, contains(actualFileNames, f), `${caption}: expected to find ${f} in ${actualFileNames}`); + export function checkArray(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { + assert.equal(actual.length, expected.length, `${caption}: incorrect actual number of files, expected:\r\n${expected.join("\r\n")}\r\ngot: ${actual.join("\r\n")}`); + for (const f of expected) { + assert.equal(true, contains(actual, f), `${caption}: expected to find ${f} in ${actual}`); } } @@ -257,8 +257,11 @@ interface Array {}` readonly watchedFiles = createMultiMap(); private readonly executingFilePath: string; private readonly currentDirectory: string; + watchDirectory: (directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean) => FileWatcher; + pollingWatchDirectoryHost: PollingWatchDirectoryHost; constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) { + this.watchDirectory = this.watchDirectoryWithCallbacks; this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); this.executingFilePath = this.getHostSpecificPath(executingFilePath); @@ -266,6 +269,10 @@ interface Array {}` this.reloadFS(fileOrFolderList); } + setWatchDirectoryWithPolling() { + this.watchDirectory = this.watchDirectoryWithPolling; + } + getNewLine() { return this.newLine; } @@ -420,9 +427,7 @@ interface Array {}` if (ignoreWatch) { return; } - if (isFile(fileOrDirectory)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); - } + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); } @@ -435,10 +440,8 @@ interface Array {}` } this.fs.delete(fileOrDirectory.path); - if (isFile(fileOrDirectory)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); - } - else { + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); + if (!isFile(fileOrDirectory)) { Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath); // Invoke directory and recursive directory watcher for the folder @@ -472,6 +475,8 @@ interface Array {}` */ private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); + // Folder is changed when the directory watcher is invoked + invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(folderFullPath)), ({ cb, fileName }) => cb(fileName, FileWatcherEventKind.Changed)); invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); } @@ -567,7 +572,7 @@ interface Array {}` }); } - watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { + watchDirectoryWithCallbacks(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { const path = this.toFullPath(directoryName); const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; const callback: TestDirectoryWatcher = { @@ -580,6 +585,19 @@ interface Array {}` }; } + getPollingWatchDirectoryHost() { + return this.pollingWatchDirectoryHost || (this.pollingWatchDirectoryHost = { + watchFile: (fileName, cb) => this.watchFile(fileName, cb), + directoryExists: path => this.directoryExists(path), + getAccessileSortedChildDirectories: path => this.getDirectories(path).sort(this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive), + filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + }); + } + + watchDirectoryWithPolling(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { + return watchDirectoryWithPolling(directoryName, recursive, this.getPollingWatchDirectoryHost(), () => cb(directoryName)); + } + createHash(s: string): string { return Harness.mockHash(s); } diff --git a/src/server/project.ts b/src/server/project.ts index 8988e8b1bec19..5c7b64055a2e3 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -856,6 +856,7 @@ namespace ts.server { const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; this.externalFiles = this.getExternalFiles(); 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. @@ -863,8 +864,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`); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index d76ff1bf6d0cd..46638b7bfb159 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -289,36 +289,6 @@ namespace ts.server { return index === 0 || value !== array[index - 1]; } - export function enumerateInsertsAndDeletes(newItems: SortedReadonlyArray, oldItems: SortedReadonlyArray, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, comparer: Comparer) { - 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;