diff --git a/lib/tsc.js b/lib/tsc.js index 152173c3bf3..0ece9fa4d41 100644 --- a/lib/tsc.js +++ b/lib/tsc.js @@ -410,8 +410,11 @@ var ts; } ts.chainDiagnosticMessages = chainDiagnosticMessages; function concatenateDiagnosticMessageChains(headChain, tailChain) { - Debug.assert(!headChain.next); - headChain.next = tailChain; + var lastChain = headChain; + while (lastChain.next) { + lastChain = lastChain.next; + } + lastChain.next = tailChain; return headChain; } ts.concatenateDiagnosticMessageChains = concatenateDiagnosticMessageChains; @@ -627,6 +630,9 @@ var ts; } ts.getRelativePathToDirectoryOrUrl = getRelativePathToDirectoryOrUrl; function getBaseFileName(path) { + if (!path) { + return undefined; + } var i = path.lastIndexOf(ts.directorySeparator); return i < 0 ? path : path.substring(i + 1); } @@ -651,6 +657,18 @@ var ts; ts.fileExtensionIs = fileExtensionIs; ts.supportedExtensions = [".ts", ".tsx", ".d.ts"]; ts.moduleFileExtensions = ts.supportedExtensions; + function isSupportedSourceFileName(fileName) { + if (!fileName) { + return false; + } + var dotIndex = fileName.lastIndexOf("."); + if (dotIndex < 0) { + return false; + } + var extension = fileName.slice(dotIndex, fileName.length); + return ts.supportedExtensions.indexOf(extension) >= 0; + } + ts.isSupportedSourceFileName = isSupportedSourceFileName; var extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"]; function removeFileExtension(path) { for (var _i = 0; _i < extensionsToRemove.length; _i++) { @@ -727,6 +745,16 @@ var ts; } Debug.fail = fail; })(Debug = ts.Debug || (ts.Debug = {})); + function copyListRemovingItem(item, list) { + var copiedList = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } + ts.copyListRemovingItem = copyListRemovingItem; })(ts || (ts = {})); var ts; (function (ts) { @@ -865,6 +893,76 @@ var ts; var _fs = require("fs"); var _path = require("path"); var _os = require("os"); + function createWatchedFileSet(interval, chunkSize) { + if (interval === void 0) { interval = 2500; } + if (chunkSize === void 0) { chunkSize = 30; } + var watchedFiles = []; + var nextFileToCheck = 0; + var watchTimer; + function getModifiedTime(fileName) { + return _fs.statSync(fileName).mtime; + } + function poll(checkedIndex) { + var watchedFile = watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + _fs.stat(watchedFile.fileName, function (err, stats) { + 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); + } + }); + } + function startWatchTimer() { + watchTimer = setInterval(function () { + var count = 0; + var nextToCheck = nextFileToCheck; + var 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, callback) { + var file = { + fileName: fileName, + callback: callback, + mtime: getModifiedTime(fileName) + }; + watchedFiles.push(file); + if (watchedFiles.length === 1) { + startWatchTimer(); + } + return file; + } + function removeFile(file) { + watchedFiles = ts.copyListRemovingItem(file, watchedFiles); + } + return { + getModifiedTime: getModifiedTime, + poll: poll, + startWatchTimer: startWatchTimer, + addFile: addFile, + removeFile: removeFile + }; + } + var watchedFileSet = createWatchedFileSet(); + function isNode4OrLater() { + return parseInt(process.version.charAt(1)) >= 4; + } var platform = _os.platform(); var useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin"; function readFile(fileName, encoding) { @@ -945,20 +1043,21 @@ var ts; readFile: readFile, writeFile: writeFile, watchFile: function (fileName, callback) { - _fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); - return { - close: function () { _fs.unwatchFile(fileName, fileChanged); } - }; - function fileChanged(curr, prev) { - if (curr.mtime.getTime() === 0) { - callback(fileName, true); - return; - } - if (+curr.mtime <= +prev.mtime) { - return; - } - callback(fileName, false); + if (isNode4OrLater()) { + return _fs.watch(fileName, function (eventName, relativeFileName) { return callback(fileName); }); } + var watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: function () { return watchedFileSet.removeFile(watchedFile); } + }; + }, + watchDirectory: function (path, callback, recursive) { + return _fs.watch(path, { persisten: true, recursive: !!recursive }, function (eventName, relativeFileName) { + if (eventName === "rename") { + callback(!relativeFileName ? relativeFileName : ts.normalizePath(ts.combinePaths(path, relativeFileName))); + } + ; + }); }, resolvePath: function (path) { return _path.resolve(path); @@ -5676,6 +5775,16 @@ var ts; } } ts.getTypeParameterOwner = getTypeParameterOwner; + function arrayStructurallyIsEqualTo(array1, array2) { + if (!array1 || !array2) { + return false; + } + if (array1.length !== array2.length) { + return false; + } + return ts.arrayIsEqualTo(array1.sort(), array2.sort()); + } + ts.arrayStructurallyIsEqualTo = arrayStructurallyIsEqualTo; })(ts || (ts = {})); var ts; (function (ts) { @@ -30924,10 +31033,10 @@ var ts; catch (e) { return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) }; } - return parseConfigFileText(fileName, text); + return parseConfigFileTextToJson(fileName, text); } ts.readConfigFile = readConfigFile; - function parseConfigFileText(fileName, jsonText) { + function parseConfigFileTextToJson(fileName, jsonText) { try { return { config: /\S/.test(jsonText) ? JSON.parse(jsonText) : {} }; } @@ -30935,8 +31044,8 @@ var ts; return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; } } - ts.parseConfigFileText = parseConfigFileText; - function parseConfigFile(json, host, basePath) { + ts.parseConfigFileTextToJson = parseConfigFileTextToJson; + function parseJsonConfigFileContent(json, host, basePath) { var errors = []; return { options: getCompilerOptions(), @@ -31021,7 +31130,7 @@ var ts; return fileNames; } } - ts.parseConfigFile = parseConfigFile; + ts.parseJsonConfigFileContent = parseJsonConfigFileContent; })(ts || (ts = {})); var ts; (function (ts) { @@ -31134,13 +31243,16 @@ var ts; function executeCommandLine(args) { var commandLine = ts.parseCommandLine(args); var configFileName; + var cachedConfigFileText; var configFileWatcher; + var directoryWatcher; var cachedProgram; var rootFileNames; var compilerOptions; var compilerHost; var hostGetSourceFile; - var timerHandle; + var timerHandleForRecompilation; + var timerHandleForDirectoryChanges; if (commandLine.options.locale) { if (!isJSONSupported()) { reportDiagnostic(ts.createCompilerDiagnostic(ts.Diagnostics.The_current_host_does_not_support_the_0_option, "--locale")); @@ -31193,22 +31305,38 @@ var ts; if (configFileName) { configFileWatcher = ts.sys.watchFile(configFileName, configFileChanged); } + if (ts.sys.watchDirectory && configFileName) { + var directory = ts.getDirectoryPath(configFileName); + directoryWatcher = ts.sys.watchDirectory(directory == "" ? "." : directory, watchedDirectoryChanged, true); + } } performCompilation(); + function parseConfigFile() { + if (!cachedConfigFileText) { + try { + cachedConfigFileText = ts.sys.readFile(configFileName); + } + catch (e) { + var error = ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); + reportWatchDiagnostic(error); + ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + } + var result = ts.parseConfigFileTextToJson(configFileName, cachedConfigFileText); + var configObject = result.config; + var configParseResult = ts.parseJsonConfigFileContent(configObject, ts.sys, ts.getDirectoryPath(configFileName)); + if (configParseResult.errors.length > 0) { + reportDiagnostics(configParseResult.errors); + ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + return configParseResult; + } function performCompilation() { if (!cachedProgram) { if (configFileName) { - var result = ts.readConfigFile(configFileName, ts.sys.readFile); - if (result.error) { - reportWatchDiagnostic(result.error); - return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - var configObject = result.config; - var configParseResult = ts.parseConfigFile(configObject, ts.sys, ts.getDirectoryPath(configFileName)); - if (configParseResult.errors.length > 0) { - reportDiagnostics(configParseResult.errors); - return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - } + var configParseResult = parseConfigFile(); rootFileNames = configParseResult.fileNames; compilerOptions = ts.extend(commandLine.options, configParseResult.options); } @@ -31263,20 +31391,42 @@ var ts; rootFileNames.splice(index, 1); } } - startTimer(); + startTimerForRecompilation(); } function configFileChanged() { setCachedProgram(undefined); - startTimer(); + cachedConfigFileText = undefined; + startTimerForRecompilation(); } - function startTimer() { - if (timerHandle) { - clearTimeout(timerHandle); + function watchedDirectoryChanged(fileName) { + if (fileName && !ts.isSupportedSourceFileName(fileName)) { + return; } - timerHandle = setTimeout(recompile, 250); + startTimerForHandlingDirectoryChanges(); + } + function startTimerForHandlingDirectoryChanges() { + if (timerHandleForDirectoryChanges) { + clearTimeout(timerHandleForDirectoryChanges); + } + timerHandleForDirectoryChanges = setTimeout(directoryChangeHandler, 250); + } + function directoryChangeHandler() { + var parsedCommandLine = parseConfigFile(); + var newFileNames = ts.map(parsedCommandLine.fileNames, compilerHost.getCanonicalFileName); + var canonicalRootFileNames = ts.map(rootFileNames, compilerHost.getCanonicalFileName); + if (!ts.arrayStructurallyIsEqualTo(newFileNames, canonicalRootFileNames)) { + setCachedProgram(undefined); + startTimerForRecompilation(); + } + } + function startTimerForRecompilation() { + if (timerHandleForRecompilation) { + clearTimeout(timerHandleForRecompilation); + } + timerHandleForRecompilation = setTimeout(recompile, 250); } function recompile() { - timerHandle = undefined; + timerHandleForRecompilation = undefined; reportWatchDiagnostic(ts.createCompilerDiagnostic(ts.Diagnostics.File_change_detected_Starting_incremental_compilation)); performCompilation(); } diff --git a/lib/tsserver.js b/lib/tsserver.js index 1b9b6cf8596..c16c75619f9 100644 --- a/lib/tsserver.js +++ b/lib/tsserver.js @@ -410,8 +410,11 @@ var ts; } ts.chainDiagnosticMessages = chainDiagnosticMessages; function concatenateDiagnosticMessageChains(headChain, tailChain) { - Debug.assert(!headChain.next); - headChain.next = tailChain; + var lastChain = headChain; + while (lastChain.next) { + lastChain = lastChain.next; + } + lastChain.next = tailChain; return headChain; } ts.concatenateDiagnosticMessageChains = concatenateDiagnosticMessageChains; @@ -627,6 +630,9 @@ var ts; } ts.getRelativePathToDirectoryOrUrl = getRelativePathToDirectoryOrUrl; function getBaseFileName(path) { + if (!path) { + return undefined; + } var i = path.lastIndexOf(ts.directorySeparator); return i < 0 ? path : path.substring(i + 1); } @@ -651,6 +657,18 @@ var ts; ts.fileExtensionIs = fileExtensionIs; ts.supportedExtensions = [".ts", ".tsx", ".d.ts"]; ts.moduleFileExtensions = ts.supportedExtensions; + function isSupportedSourceFileName(fileName) { + if (!fileName) { + return false; + } + var dotIndex = fileName.lastIndexOf("."); + if (dotIndex < 0) { + return false; + } + var extension = fileName.slice(dotIndex, fileName.length); + return ts.supportedExtensions.indexOf(extension) >= 0; + } + ts.isSupportedSourceFileName = isSupportedSourceFileName; var extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"]; function removeFileExtension(path) { for (var _i = 0; _i < extensionsToRemove.length; _i++) { @@ -727,6 +745,16 @@ var ts; } Debug.fail = fail; })(Debug = ts.Debug || (ts.Debug = {})); + function copyListRemovingItem(item, list) { + var copiedList = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } + ts.copyListRemovingItem = copyListRemovingItem; })(ts || (ts = {})); var ts; (function (ts) { @@ -865,6 +893,76 @@ var ts; var _fs = require("fs"); var _path = require("path"); var _os = require("os"); + function createWatchedFileSet(interval, chunkSize) { + if (interval === void 0) { interval = 2500; } + if (chunkSize === void 0) { chunkSize = 30; } + var watchedFiles = []; + var nextFileToCheck = 0; + var watchTimer; + function getModifiedTime(fileName) { + return _fs.statSync(fileName).mtime; + } + function poll(checkedIndex) { + var watchedFile = watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + _fs.stat(watchedFile.fileName, function (err, stats) { + 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); + } + }); + } + function startWatchTimer() { + watchTimer = setInterval(function () { + var count = 0; + var nextToCheck = nextFileToCheck; + var 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, callback) { + var file = { + fileName: fileName, + callback: callback, + mtime: getModifiedTime(fileName) + }; + watchedFiles.push(file); + if (watchedFiles.length === 1) { + startWatchTimer(); + } + return file; + } + function removeFile(file) { + watchedFiles = ts.copyListRemovingItem(file, watchedFiles); + } + return { + getModifiedTime: getModifiedTime, + poll: poll, + startWatchTimer: startWatchTimer, + addFile: addFile, + removeFile: removeFile + }; + } + var watchedFileSet = createWatchedFileSet(); + function isNode4OrLater() { + return parseInt(process.version.charAt(1)) >= 4; + } var platform = _os.platform(); var useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin"; function readFile(fileName, encoding) { @@ -945,20 +1043,21 @@ var ts; readFile: readFile, writeFile: writeFile, watchFile: function (fileName, callback) { - _fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); - return { - close: function () { _fs.unwatchFile(fileName, fileChanged); } - }; - function fileChanged(curr, prev) { - if (curr.mtime.getTime() === 0) { - callback(fileName, true); - return; - } - if (+curr.mtime <= +prev.mtime) { - return; - } - callback(fileName, false); + if (isNode4OrLater()) { + return _fs.watch(fileName, function (eventName, relativeFileName) { return callback(fileName); }); } + var watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: function () { return watchedFileSet.removeFile(watchedFile); } + }; + }, + watchDirectory: function (path, callback, recursive) { + return _fs.watch(path, { persisten: true, recursive: !!recursive }, function (eventName, relativeFileName) { + if (eventName === "rename") { + callback(!relativeFileName ? relativeFileName : ts.normalizePath(ts.combinePaths(path, relativeFileName))); + } + ; + }); }, resolvePath: function (path) { return _path.resolve(path); @@ -3338,10 +3437,10 @@ var ts; catch (e) { return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) }; } - return parseConfigFileText(fileName, text); + return parseConfigFileTextToJson(fileName, text); } ts.readConfigFile = readConfigFile; - function parseConfigFileText(fileName, jsonText) { + function parseConfigFileTextToJson(fileName, jsonText) { try { return { config: /\S/.test(jsonText) ? JSON.parse(jsonText) : {} }; } @@ -3349,8 +3448,8 @@ var ts; return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; } } - ts.parseConfigFileText = parseConfigFileText; - function parseConfigFile(json, host, basePath) { + ts.parseConfigFileTextToJson = parseConfigFileTextToJson; + function parseJsonConfigFileContent(json, host, basePath) { var errors = []; return { options: getCompilerOptions(), @@ -3435,7 +3534,7 @@ var ts; return fileNames; } } - ts.parseConfigFile = parseConfigFile; + ts.parseJsonConfigFileContent = parseJsonConfigFileContent; })(ts || (ts = {})); var ts; (function (ts) { @@ -5403,6 +5502,16 @@ var ts; } } ts.getTypeParameterOwner = getTypeParameterOwner; + function arrayStructurallyIsEqualTo(array1, array2) { + if (!array1 || !array2) { + return false; + } + if (array1.length !== array2.length) { + return false; + } + return ts.arrayIsEqualTo(array1.sort(), array2.sort()); + } + ts.arrayStructurallyIsEqualTo = arrayStructurallyIsEqualTo; })(ts || (ts = {})); var ts; (function (ts) { @@ -41186,6 +41295,22 @@ var ts; return spaceCache[n]; } server.generateSpaces = generateSpaces; + function generateIndentString(n, editorOptions) { + if (editorOptions.ConvertTabsToSpaces) { + return generateSpaces(n); + } + else { + var result = ""; + for (var i = 0; i < Math.floor(n / editorOptions.TabSize); i++) { + result += "\t"; + } + for (var i = 0; i < n % editorOptions.TabSize; i++) { + result += " "; + } + return result; + } + } + server.generateIndentString = generateIndentString; function compareNumber(a, b) { if (a < b) { return -1; @@ -41800,26 +41925,24 @@ var ts; ConvertTabsToSpaces: formatOptions.ConvertTabsToSpaces, IndentStyle: ts.IndentStyle.Smart }; - var indentPosition = compilerService.languageService.getIndentationAtPosition(file, position, editorOptions); + var preferredIndent = compilerService.languageService.getIndentationAtPosition(file, position, editorOptions); + var hasIndent = 0; for (var i = 0, len = lineText.length; i < len; i++) { if (lineText.charAt(i) == " ") { - indentPosition--; + hasIndent++; } else if (lineText.charAt(i) == "\t") { - indentPosition -= editorOptions.IndentSize; + hasIndent += editorOptions.TabSize; } else { break; } } - if (indentPosition > 0) { - var spaces = generateSpaces(indentPosition); - edits.push({ span: ts.createTextSpanFromBounds(position, position), newText: spaces }); - } - else if (indentPosition < 0) { + if (preferredIndent !== hasIndent) { + var firstNoWhiteSpacePosition = lineInfo.offset + i; edits.push({ - span: ts.createTextSpanFromBounds(position, position - indentPosition), - newText: "" + span: ts.createTextSpanFromBounds(lineInfo.offset, firstNoWhiteSpacePosition), + newText: generateIndentString(preferredIndent, editorOptions) }); } } @@ -41951,6 +42074,9 @@ var ts; } }; Session.prototype.closeClientFile = function (fileName) { + if (!fileName) { + return; + } var file = ts.normalizePath(fileName); this.projectService.closeClientFile(file); }; @@ -42307,6 +42433,7 @@ var ts; if (scriptInfo) { this.filenameToScript[info.fileName] = undefined; this.roots = copyListRemovingItem(info, this.roots); + this.resolvedModuleNames.remove(info.fileName); } }; LSHost.prototype.saveTo = function (filename, tmpfilename) { @@ -42400,6 +42527,7 @@ var ts; function Project(projectService, projectOptions) { this.projectService = projectService; this.projectOptions = projectOptions; + this.directoriesWatchedForTsconfig = []; this.filenameToSourceFile = {}; this.updateGraphSeq = 0; this.openRefCount = 0; @@ -42415,6 +42543,9 @@ var ts; Project.prototype.openReferencedFile = function (filename) { return this.projectService.openFile(filename, false); }; + Project.prototype.getRootFiles = function () { + return this.compilerService.host.roots.map(function (info) { return info.fileName; }); + }; Project.prototype.getFileNames = function () { var sourceFiles = this.program.getSourceFiles(); return sourceFiles.map(function (sourceFile) { return sourceFile.fileName; }); @@ -42457,11 +42588,9 @@ var ts; return this.projectFilename; }; Project.prototype.addRoot = function (info) { - info.defaultProject = this; this.compilerService.host.addRoot(info); }; Project.prototype.removeRoot = function (info) { - info.defaultProject = undefined; this.compilerService.host.removeRoot(info); }; Project.prototype.filesToString = function () { @@ -42498,6 +42627,9 @@ var ts; this.configuredProjects = []; this.openFilesReferenced = []; this.openFileRootsConfigured = []; + this.directoryWatchersForTsconfig = {}; + this.directoryWatchersRefCount = {}; + this.timerForDetectingProjectFilelistChanges = {}; this.addDefaultHostConfiguration(); } ProjectService.prototype.addDefaultHostConfiguration = function () { @@ -42529,8 +42661,54 @@ var ts; } } }; + ProjectService.prototype.directoryWatchedForSourceFilesChanged = function (project, fileName) { + if (fileName && !ts.isSupportedSourceFileName(fileName)) { + return; + } + this.log("Detected source file changes: " + fileName); + this.startTimerForDetectingProjectFilelistChanges(project); + }; + ProjectService.prototype.startTimerForDetectingProjectFilelistChanges = function (project) { + var _this = this; + if (this.timerForDetectingProjectFilelistChanges[project.projectFilename]) { + clearTimeout(this.timerForDetectingProjectFilelistChanges[project.projectFilename]); + } + this.timerForDetectingProjectFilelistChanges[project.projectFilename] = setTimeout(function () { return _this.handleProjectFilelistChanges(project); }, 250); + }; + ProjectService.prototype.handleProjectFilelistChanges = function (project) { + var _this = this; + var _a = this.configFileToProjectOptions(project.projectFilename), succeeded = _a.succeeded, projectOptions = _a.projectOptions, error = _a.error; + var newRootFiles = projectOptions.files.map((function (f) { return _this.getCanonicalFileName(f); })); + var currentRootFiles = project.getRootFiles().map((function (f) { return _this.getCanonicalFileName(f); })); + if (!ts.arrayStructurallyIsEqualTo(currentRootFiles, newRootFiles)) { + this.updateConfiguredProject(project); + this.updateProjectStructure(); + } + }; + ProjectService.prototype.directoryWatchedForTsconfigChanged = function (fileName) { + var _this = this; + if (ts.getBaseFileName(fileName) != "tsconfig.json") { + this.log(fileName + " is not tsconfig.json"); + return; + } + this.log("Detected newly added tsconfig file: " + fileName); + var _a = this.configFileToProjectOptions(fileName), succeeded = _a.succeeded, projectOptions = _a.projectOptions, error = _a.error; + var rootFilesInTsconfig = projectOptions.files.map(function (f) { return _this.getCanonicalFileName(f); }); + var openFileRoots = this.openFileRoots.map(function (s) { return _this.getCanonicalFileName(s.fileName); }); + for (var _i = 0; _i < openFileRoots.length; _i++) { + var openFileRoot = openFileRoots[_i]; + if (rootFilesInTsconfig.indexOf(openFileRoot) >= 0) { + this.reloadProjects(); + return; + } + } + }; + ProjectService.prototype.getCanonicalFileName = function (fileName) { + var name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); + return ts.normalizePath(name); + }; ProjectService.prototype.watchedProjectConfigFileChanged = function (project) { - this.log("Config File Changed: " + project.projectFilename); + this.log("Config file changed: " + project.projectFilename); this.updateConfiguredProject(project); this.updateProjectStructure(); }; @@ -42561,11 +42739,28 @@ var ts; this.psLogger.close(); }; ProjectService.prototype.createInferredProject = function (root) { - var iproj = new Project(this); - iproj.addRoot(root); - iproj.finishGraph(); - this.inferredProjects.push(iproj); - return iproj; + var _this = this; + var project = new Project(this); + project.addRoot(root); + var currentPath = ts.getDirectoryPath(root.fileName); + var parentPath = ts.getDirectoryPath(currentPath); + while (currentPath != parentPath) { + if (!project.projectService.directoryWatchersForTsconfig[currentPath]) { + this.log("Add watcher for: " + currentPath); + project.projectService.directoryWatchersForTsconfig[currentPath] = + this.host.watchDirectory(currentPath, function (fileName) { return _this.directoryWatchedForTsconfigChanged(fileName); }); + project.projectService.directoryWatchersRefCount[currentPath] = 1; + } + else { + project.projectService.directoryWatchersRefCount[currentPath] += 1; + } + project.directoriesWatchedForTsconfig.push(currentPath); + currentPath = parentPath; + parentPath = ts.getDirectoryPath(parentPath); + } + project.finishGraph(); + this.inferredProjects.push(project); + return project; }; ProjectService.prototype.fileDeletedInFilesystem = function (info) { this.psLogger.info(info.fileName + " deleted"); @@ -42576,6 +42771,9 @@ var ts; if (!info.isOpen) { this.filenameToScriptInfo[info.fileName] = undefined; var referencingProjects = this.findReferencingProjects(info); + if (info.defaultProject) { + info.defaultProject.removeRoot(info); + } for (var i = 0, len = referencingProjects.length; i < len; i++) { referencingProjects[i].removeReferencedFile(info); } @@ -42603,12 +42801,27 @@ var ts; } this.configuredProjects = configuredProjects; }; - ProjectService.prototype.removeConfiguredProject = function (project) { - project.projectFileWatcher.close(); - this.configuredProjects = copyListRemovingItem(project, this.configuredProjects); + ProjectService.prototype.removeProject = function (project) { + this.log("remove project: " + project.getRootFiles().toString()); + if (project.isConfiguredProject()) { + project.projectFileWatcher.close(); + project.directoryWatcher.close(); + this.configuredProjects = copyListRemovingItem(project, this.configuredProjects); + } + else { + for (var _i = 0, _a = project.directoriesWatchedForTsconfig; _i < _a.length; _i++) { + var directory = _a[_i]; + if (!(--project.projectService.directoryWatchersRefCount[directory])) { + this.log("Close directory watcher for: " + directory); + project.projectService.directoryWatchersForTsconfig[directory].close(); + project.projectService.directoryWatchersForTsconfig[directory] = undefined; + } + } + this.inferredProjects = copyListRemovingItem(project, this.inferredProjects); + } var fileNames = project.getFileNames(); - for (var _i = 0; _i < fileNames.length; _i++) { - var fileName = fileNames[_i]; + for (var _b = 0; _b < fileNames.length; _b++) { + var fileName = fileNames[_b]; var info = this.getScriptInfo(fileName); if (info.defaultProject == project) { info.defaultProject = undefined; @@ -42641,8 +42854,7 @@ var ts; for (var i = 0, len = this.openFileRoots.length; i < len; i++) { var r = this.openFileRoots[i]; if (info.defaultProject.getSourceFile(r)) { - this.inferredProjects = - copyListRemovingItem(r.defaultProject, this.inferredProjects); + this.removeProject(r.defaultProject); this.openFilesReferenced.push(r); r.defaultProject = info.defaultProject; } @@ -42657,6 +42869,7 @@ var ts; this.updateConfiguredProjectList(); }; ProjectService.prototype.closeOpenFile = function (info) { + info.svc.reloadFromFile(info.fileName); var openFileRoots = []; var removedProject; for (var i = 0, len = this.openFileRoots.length; i < len; i++) { @@ -42683,17 +42896,12 @@ var ts; this.openFileRootsConfigured = openFileRootsConfigured; } if (removedProject) { - if (removedProject.isConfiguredProject()) { - this.configuredProjects = copyListRemovingItem(removedProject, this.configuredProjects); - } - else { - this.inferredProjects = copyListRemovingItem(removedProject, this.inferredProjects); - } + this.removeProject(removedProject); var openFilesReferenced = []; var orphanFiles = []; for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) { var f = this.openFilesReferenced[i]; - if (f.defaultProject === removedProject) { + if (f.defaultProject === removedProject || !f.defaultProject) { f.defaultProject = undefined; orphanFiles.push(f); } @@ -42734,6 +42942,7 @@ var ts; return referencingProjects; }; ProjectService.prototype.reloadProjects = function () { + this.log("reload projects."); for (var _i = 0, _a = this.openFileRoots; _i < _a.length; _i++) { var info = _a[_i]; this.openOrUpdateConfiguredProjectForFile(info.fileName); @@ -42775,13 +42984,21 @@ var ts; var rootFile = this.openFileRoots[i]; var rootedProject = rootFile.defaultProject; var referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - if (referencingProjects.length === 0) { - rootFile.defaultProject = rootedProject; - openFileRoots.push(rootFile); + if (rootFile.defaultProject && rootFile.defaultProject.isConfiguredProject()) { + if (!rootedProject.isConfiguredProject()) { + this.removeProject(rootedProject); + } + this.openFileRootsConfigured.push(rootFile); } else { - this.inferredProjects = copyListRemovingItem(rootedProject, this.inferredProjects); - this.openFilesReferenced.push(rootFile); + if (referencingProjects.length === 0) { + rootFile.defaultProject = rootedProject; + openFileRoots.push(rootFile); + } + else { + this.removeProject(rootedProject); + this.openFilesReferenced.push(rootFile); + } } } this.openFileRoots = openFileRoots; @@ -42952,12 +43169,12 @@ var ts; configFilename = ts.normalizePath(configFilename); var dirPath = ts.getDirectoryPath(configFilename); var contents = this.host.readFile(configFilename); - var rawConfig = ts.parseConfigFileText(configFilename, contents); + var rawConfig = ts.parseConfigFileTextToJson(configFilename, contents); if (rawConfig.error) { return { succeeded: false, error: rawConfig.error }; } else { - var parsedCommandLine = ts.parseConfigFile(rawConfig.config, this.host, dirPath); + var parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath); if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { return { succeeded: false, error: { errorMsg: "tsconfig option errors" } }; } @@ -42980,26 +43197,28 @@ var ts; return error; } else { - var proj = this.createProject(configFilename, projectOptions); - for (var i = 0, len = projectOptions.files.length; i < len; i++) { - var rootFilename = projectOptions.files[i]; + var project = this.createProject(configFilename, projectOptions); + for (var _i = 0, _b = projectOptions.files; _i < _b.length; _i++) { + var rootFilename = _b[_i]; if (this.host.fileExists(rootFilename)) { var info = this.openFile(rootFilename, clientFileName == rootFilename); - proj.addRoot(info); + project.addRoot(info); } else { return { errorMsg: "specified file " + rootFilename + " not found" }; } } - proj.finishGraph(); - proj.projectFileWatcher = this.host.watchFile(configFilename, function (_) { return _this.watchedProjectConfigFileChanged(proj); }); - return { success: true, project: proj }; + project.finishGraph(); + project.projectFileWatcher = this.host.watchFile(configFilename, function (_) { return _this.watchedProjectConfigFileChanged(project); }); + this.log("Add recursive watcher for: " + ts.getDirectoryPath(configFilename)); + project.directoryWatcher = this.host.watchDirectory(ts.getDirectoryPath(configFilename), function (path) { return _this.directoryWatchedForSourceFilesChanged(project, path); }, true); + return { success: true, project: project }; } }; ProjectService.prototype.updateConfiguredProject = function (project) { if (!this.host.fileExists(project.projectFilename)) { this.log("Config file deleted"); - this.removeConfiguredProject(project); + this.removeProject(project); } else { var _a = this.configFileToProjectOptions(project.projectFilename), succeeded = _a.succeeded, projectOptions = _a.projectOptions, error = _a.error; @@ -43014,7 +43233,9 @@ var ts; for (var _i = 0; _i < fileNamesToRemove.length; _i++) { var fileName = fileNamesToRemove[_i]; var info = this.getScriptInfo(fileName); - project.removeRoot(info); + if (info) { + project.removeRoot(info); + } } for (var _b = 0; _b < fileNamesToAdd.length; _b++) { var fileName = fileNamesToAdd[_b]; @@ -43991,79 +44212,6 @@ var ts; }; return Logger; })(); - var WatchedFileSet = (function () { - function WatchedFileSet(interval, chunkSize) { - if (interval === void 0) { interval = 2500; } - if (chunkSize === void 0) { chunkSize = 30; } - this.interval = interval; - this.chunkSize = chunkSize; - this.watchedFiles = []; - this.nextFileToCheck = 0; - } - WatchedFileSet.copyListRemovingItem = function (item, list) { - var copiedList = []; - for (var i = 0, len = list.length; i < len; i++) { - if (list[i] != item) { - copiedList.push(list[i]); - } - } - return copiedList; - }; - WatchedFileSet.getModifiedTime = function (fileName) { - return fs.statSync(fileName).mtime; - }; - WatchedFileSet.prototype.poll = function (checkedIndex) { - var watchedFile = this.watchedFiles[checkedIndex]; - if (!watchedFile) { - return; - } - fs.stat(watchedFile.fileName, function (err, stats) { - if (err) { - watchedFile.callback(watchedFile.fileName, false); - } - else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { - watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName); - watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); - } - }); - }; - WatchedFileSet.prototype.startWatchTimer = function () { - var _this = this; - this.watchTimer = setInterval(function () { - var count = 0; - var nextToCheck = _this.nextFileToCheck; - var firstCheck = -1; - while ((count < _this.chunkSize) && (nextToCheck !== firstCheck)) { - _this.poll(nextToCheck); - if (firstCheck < 0) { - firstCheck = nextToCheck; - } - nextToCheck++; - if (nextToCheck === _this.watchedFiles.length) { - nextToCheck = 0; - } - count++; - } - _this.nextFileToCheck = nextToCheck; - }, this.interval); - }; - WatchedFileSet.prototype.addFile = function (fileName, callback) { - var file = { - fileName: fileName, - callback: callback, - mtime: WatchedFileSet.getModifiedTime(fileName) - }; - this.watchedFiles.push(file); - if (this.watchedFiles.length === 1) { - this.startWatchTimer(); - } - return file; - }; - WatchedFileSet.prototype.removeFile = function (file) { - this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles); - }; - return WatchedFileSet; - })(); var IOSession = (function (_super) { __extends(IOSession, _super); function IOSession(host, logger) { @@ -44124,13 +44272,6 @@ var ts; return new Logger(fileName, detailLevel); } var logger = createLoggerFromEnv(); - var watchedFileSet = new WatchedFileSet(); - ts.sys.watchFile = function (fileName, callback) { - var watchedFile = watchedFileSet.addFile(fileName, callback); - return { - close: function () { return watchedFileSet.removeFile(watchedFile); } - }; - }; var ioSession = new IOSession(ts.sys, logger); process.on('uncaughtException', function (err) { ioSession.logError(err, "unknown"); @@ -44697,7 +44838,7 @@ var ts; var _this = this; return this.forwardJSONCall("getTSConfigFileInfo('" + fileName + "')", function () { var text = sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()); - var result = ts.parseConfigFileText(fileName, text); + var result = ts.parseConfigFileTextToJson(fileName, text); if (result.error) { return { options: {}, @@ -44705,7 +44846,7 @@ var ts; errors: [realizeDiagnostic(result.error, '\r\n')] }; } - var configFile = ts.parseConfigFile(result.config, _this.host, ts.getDirectoryPath(ts.normalizeSlashes(fileName))); + var configFile = ts.parseJsonConfigFileContent(result.config, _this.host, ts.getDirectoryPath(ts.normalizeSlashes(fileName))); return { options: configFile.options, files: configFile.fileNames, diff --git a/lib/typescript.d.ts b/lib/typescript.d.ts index cc794465080..2c68f00e89a 100644 --- a/lib/typescript.d.ts +++ b/lib/typescript.d.ts @@ -1428,7 +1428,8 @@ declare 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; @@ -1518,6 +1519,7 @@ declare namespace ts { */ function collapseTextChangeRangesAcrossMultipleVersions(changes: TextChangeRange[]): TextChangeRange; function getTypeParameterOwner(d: Declaration): Declaration; + function arrayStructurallyIsEqualTo(array1: Array, array2: Array): boolean; } declare namespace ts { function getNodeConstructor(kind: SyntaxKind): new () => Node; @@ -1553,7 +1555,7 @@ declare namespace ts { * @param fileName The path to the config file * @param jsonText The text of the config file */ - function parseConfigFileText(fileName: string, jsonText: string): { + function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic; }; @@ -1563,7 +1565,7 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseConfigFile(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine; + function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine; } declare namespace ts { /** The version of the language service API */ diff --git a/lib/typescript.js b/lib/typescript.js index 65d58efa4fb..9352ba682e4 100644 --- a/lib/typescript.js +++ b/lib/typescript.js @@ -1226,8 +1226,11 @@ var ts; } ts.chainDiagnosticMessages = chainDiagnosticMessages; function concatenateDiagnosticMessageChains(headChain, tailChain) { - Debug.assert(!headChain.next); - headChain.next = tailChain; + var lastChain = headChain; + while (lastChain.next) { + lastChain = lastChain.next; + } + lastChain.next = tailChain; return headChain; } ts.concatenateDiagnosticMessageChains = concatenateDiagnosticMessageChains; @@ -1475,6 +1478,9 @@ var ts; } ts.getRelativePathToDirectoryOrUrl = getRelativePathToDirectoryOrUrl; function getBaseFileName(path) { + if (!path) { + return undefined; + } var i = path.lastIndexOf(ts.directorySeparator); return i < 0 ? path : path.substring(i + 1); } @@ -1507,6 +1513,18 @@ var ts; * but still would like to load only TypeScript files as modules */ ts.moduleFileExtensions = ts.supportedExtensions; + function isSupportedSourceFileName(fileName) { + if (!fileName) { + return false; + } + var dotIndex = fileName.lastIndexOf("."); + if (dotIndex < 0) { + return false; + } + var extension = fileName.slice(dotIndex, fileName.length); + return ts.supportedExtensions.indexOf(extension) >= 0; + } + ts.isSupportedSourceFileName = isSupportedSourceFileName; var extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"]; function removeFileExtension(path) { for (var _i = 0; _i < extensionsToRemove.length; _i++) { @@ -1590,6 +1608,16 @@ var ts; } Debug.fail = fail; })(Debug = ts.Debug || (ts.Debug = {})); + function copyListRemovingItem(item, list) { + var copiedList = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } + ts.copyListRemovingItem = copyListRemovingItem; })(ts || (ts = {})); /// var ts; @@ -1736,6 +1764,94 @@ var ts; var _fs = require("fs"); var _path = require("path"); var _os = require("os"); + // average async stat takes about 30 microseconds + // set chunk size to do 30 files in < 1 millisecond + function createWatchedFileSet(interval, chunkSize) { + if (interval === void 0) { interval = 2500; } + if (chunkSize === void 0) { chunkSize = 30; } + var watchedFiles = []; + var nextFileToCheck = 0; + var watchTimer; + function getModifiedTime(fileName) { + return _fs.statSync(fileName).mtime; + } + function poll(checkedIndex) { + var watchedFile = watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + _fs.stat(watchedFile.fileName, function (err, stats) { + 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(function () { + var count = 0; + var nextToCheck = nextFileToCheck; + var 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, callback) { + var file = { + fileName: fileName, + callback: callback, + mtime: getModifiedTime(fileName) + }; + watchedFiles.push(file); + if (watchedFiles.length === 1) { + startWatchTimer(); + } + return file; + } + function removeFile(file) { + watchedFiles = ts.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? + var watchedFileSet = createWatchedFileSet(); + function isNode4OrLater() { + return parseInt(process.version.charAt(1)) >= 4; + } var platform = _os.platform(); // win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive var useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin"; @@ -1824,22 +1940,32 @@ var ts; readFile: readFile, writeFile: writeFile, watchFile: function (fileName, callback) { - // watchFile polls a file every 250ms, picking up file notifications. - _fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); - return { - close: function () { _fs.unwatchFile(fileName, fileChanged); } - }; - function fileChanged(curr, prev) { - // mtime.getTime() equals 0 if file was removed - if (curr.mtime.getTime() === 0) { - callback(fileName, /* removed */ true); - return; - } - if (+curr.mtime <= +prev.mtime) { - return; - } - callback(fileName, /* removed */ false); + // 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, function (eventName, relativeFileName) { return callback(fileName); }); } + var watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: function () { return watchedFileSet.removeFile(watchedFile); } + }; + }, + watchDirectory: function (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 }, function (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 : ts.normalizePath(ts.combinePaths(path, relativeFileName))); + } + ; + }); }, resolvePath: function (path) { return _path.resolve(path); @@ -7243,6 +7369,16 @@ var ts; } } ts.getTypeParameterOwner = getTypeParameterOwner; + function arrayStructurallyIsEqualTo(array1, array2) { + if (!array1 || !array2) { + return false; + } + if (array1.length !== array2.length) { + return false; + } + return ts.arrayIsEqualTo(array1.sort(), array2.sort()); + } + ts.arrayStructurallyIsEqualTo = arrayStructurallyIsEqualTo; })(ts || (ts = {})); /// /// @@ -37106,7 +37242,7 @@ var ts; catch (e) { return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) }; } - return parseConfigFileText(fileName, text); + return parseConfigFileTextToJson(fileName, text); } ts.readConfigFile = readConfigFile; /** @@ -37114,7 +37250,7 @@ var ts; * @param fileName The path to the config file * @param jsonText The text of the config file */ - function parseConfigFileText(fileName, jsonText) { + function parseConfigFileTextToJson(fileName, jsonText) { try { return { config: /\S/.test(jsonText) ? JSON.parse(jsonText) : {} }; } @@ -37122,14 +37258,14 @@ var ts; return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; } } - ts.parseConfigFileText = parseConfigFileText; + ts.parseConfigFileTextToJson = parseConfigFileTextToJson; /** * Parse the contents of a config file (tsconfig.json). * @param json The contents of the config file to parse * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseConfigFile(json, host, basePath) { + function parseJsonConfigFileContent(json, host, basePath) { var errors = []; return { options: getCompilerOptions(), @@ -37214,7 +37350,7 @@ var ts; return fileNames; } } - ts.parseConfigFile = parseConfigFile; + ts.parseJsonConfigFileContent = parseJsonConfigFileContent; })(ts || (ts = {})); /* @internal */ var ts; @@ -50072,7 +50208,7 @@ var ts; var _this = this; return this.forwardJSONCall("getTSConfigFileInfo('" + fileName + "')", function () { var text = sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()); - var result = ts.parseConfigFileText(fileName, text); + var result = ts.parseConfigFileTextToJson(fileName, text); if (result.error) { return { options: {}, @@ -50080,7 +50216,7 @@ var ts; errors: [realizeDiagnostic(result.error, '\r\n')] }; } - var configFile = ts.parseConfigFile(result.config, _this.host, ts.getDirectoryPath(ts.normalizeSlashes(fileName))); + var configFile = ts.parseJsonConfigFileContent(result.config, _this.host, ts.getDirectoryPath(ts.normalizeSlashes(fileName))); return { options: configFile.options, files: configFile.fileNames, diff --git a/lib/typescriptServices.d.ts b/lib/typescriptServices.d.ts index 5ecd1c287ef..6031ff46f38 100644 --- a/lib/typescriptServices.d.ts +++ b/lib/typescriptServices.d.ts @@ -1428,7 +1428,8 @@ declare 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; @@ -1518,6 +1519,7 @@ declare namespace ts { */ function collapseTextChangeRangesAcrossMultipleVersions(changes: TextChangeRange[]): TextChangeRange; function getTypeParameterOwner(d: Declaration): Declaration; + function arrayStructurallyIsEqualTo(array1: Array, array2: Array): boolean; } declare namespace ts { function getNodeConstructor(kind: SyntaxKind): new () => Node; @@ -1553,7 +1555,7 @@ declare namespace ts { * @param fileName The path to the config file * @param jsonText The text of the config file */ - function parseConfigFileText(fileName: string, jsonText: string): { + function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic; }; @@ -1563,7 +1565,7 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseConfigFile(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine; + function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine; } declare namespace ts { /** The version of the language service API */ diff --git a/lib/typescriptServices.js b/lib/typescriptServices.js index 65d58efa4fb..9352ba682e4 100644 --- a/lib/typescriptServices.js +++ b/lib/typescriptServices.js @@ -1226,8 +1226,11 @@ var ts; } ts.chainDiagnosticMessages = chainDiagnosticMessages; function concatenateDiagnosticMessageChains(headChain, tailChain) { - Debug.assert(!headChain.next); - headChain.next = tailChain; + var lastChain = headChain; + while (lastChain.next) { + lastChain = lastChain.next; + } + lastChain.next = tailChain; return headChain; } ts.concatenateDiagnosticMessageChains = concatenateDiagnosticMessageChains; @@ -1475,6 +1478,9 @@ var ts; } ts.getRelativePathToDirectoryOrUrl = getRelativePathToDirectoryOrUrl; function getBaseFileName(path) { + if (!path) { + return undefined; + } var i = path.lastIndexOf(ts.directorySeparator); return i < 0 ? path : path.substring(i + 1); } @@ -1507,6 +1513,18 @@ var ts; * but still would like to load only TypeScript files as modules */ ts.moduleFileExtensions = ts.supportedExtensions; + function isSupportedSourceFileName(fileName) { + if (!fileName) { + return false; + } + var dotIndex = fileName.lastIndexOf("."); + if (dotIndex < 0) { + return false; + } + var extension = fileName.slice(dotIndex, fileName.length); + return ts.supportedExtensions.indexOf(extension) >= 0; + } + ts.isSupportedSourceFileName = isSupportedSourceFileName; var extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"]; function removeFileExtension(path) { for (var _i = 0; _i < extensionsToRemove.length; _i++) { @@ -1590,6 +1608,16 @@ var ts; } Debug.fail = fail; })(Debug = ts.Debug || (ts.Debug = {})); + function copyListRemovingItem(item, list) { + var copiedList = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } + ts.copyListRemovingItem = copyListRemovingItem; })(ts || (ts = {})); /// var ts; @@ -1736,6 +1764,94 @@ var ts; var _fs = require("fs"); var _path = require("path"); var _os = require("os"); + // average async stat takes about 30 microseconds + // set chunk size to do 30 files in < 1 millisecond + function createWatchedFileSet(interval, chunkSize) { + if (interval === void 0) { interval = 2500; } + if (chunkSize === void 0) { chunkSize = 30; } + var watchedFiles = []; + var nextFileToCheck = 0; + var watchTimer; + function getModifiedTime(fileName) { + return _fs.statSync(fileName).mtime; + } + function poll(checkedIndex) { + var watchedFile = watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + _fs.stat(watchedFile.fileName, function (err, stats) { + 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(function () { + var count = 0; + var nextToCheck = nextFileToCheck; + var 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, callback) { + var file = { + fileName: fileName, + callback: callback, + mtime: getModifiedTime(fileName) + }; + watchedFiles.push(file); + if (watchedFiles.length === 1) { + startWatchTimer(); + } + return file; + } + function removeFile(file) { + watchedFiles = ts.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? + var watchedFileSet = createWatchedFileSet(); + function isNode4OrLater() { + return parseInt(process.version.charAt(1)) >= 4; + } var platform = _os.platform(); // win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive var useCaseSensitiveFileNames = platform !== "win32" && platform !== "win64" && platform !== "darwin"; @@ -1824,22 +1940,32 @@ var ts; readFile: readFile, writeFile: writeFile, watchFile: function (fileName, callback) { - // watchFile polls a file every 250ms, picking up file notifications. - _fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); - return { - close: function () { _fs.unwatchFile(fileName, fileChanged); } - }; - function fileChanged(curr, prev) { - // mtime.getTime() equals 0 if file was removed - if (curr.mtime.getTime() === 0) { - callback(fileName, /* removed */ true); - return; - } - if (+curr.mtime <= +prev.mtime) { - return; - } - callback(fileName, /* removed */ false); + // 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, function (eventName, relativeFileName) { return callback(fileName); }); } + var watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: function () { return watchedFileSet.removeFile(watchedFile); } + }; + }, + watchDirectory: function (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 }, function (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 : ts.normalizePath(ts.combinePaths(path, relativeFileName))); + } + ; + }); }, resolvePath: function (path) { return _path.resolve(path); @@ -7243,6 +7369,16 @@ var ts; } } ts.getTypeParameterOwner = getTypeParameterOwner; + function arrayStructurallyIsEqualTo(array1, array2) { + if (!array1 || !array2) { + return false; + } + if (array1.length !== array2.length) { + return false; + } + return ts.arrayIsEqualTo(array1.sort(), array2.sort()); + } + ts.arrayStructurallyIsEqualTo = arrayStructurallyIsEqualTo; })(ts || (ts = {})); /// /// @@ -37106,7 +37242,7 @@ var ts; catch (e) { return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) }; } - return parseConfigFileText(fileName, text); + return parseConfigFileTextToJson(fileName, text); } ts.readConfigFile = readConfigFile; /** @@ -37114,7 +37250,7 @@ var ts; * @param fileName The path to the config file * @param jsonText The text of the config file */ - function parseConfigFileText(fileName, jsonText) { + function parseConfigFileTextToJson(fileName, jsonText) { try { return { config: /\S/.test(jsonText) ? JSON.parse(jsonText) : {} }; } @@ -37122,14 +37258,14 @@ var ts; return { error: ts.createCompilerDiagnostic(ts.Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; } } - ts.parseConfigFileText = parseConfigFileText; + ts.parseConfigFileTextToJson = parseConfigFileTextToJson; /** * Parse the contents of a config file (tsconfig.json). * @param json The contents of the config file to parse * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseConfigFile(json, host, basePath) { + function parseJsonConfigFileContent(json, host, basePath) { var errors = []; return { options: getCompilerOptions(), @@ -37214,7 +37350,7 @@ var ts; return fileNames; } } - ts.parseConfigFile = parseConfigFile; + ts.parseJsonConfigFileContent = parseJsonConfigFileContent; })(ts || (ts = {})); /* @internal */ var ts; @@ -50072,7 +50208,7 @@ var ts; var _this = this; return this.forwardJSONCall("getTSConfigFileInfo('" + fileName + "')", function () { var text = sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()); - var result = ts.parseConfigFileText(fileName, text); + var result = ts.parseConfigFileTextToJson(fileName, text); if (result.error) { return { options: {}, @@ -50080,7 +50216,7 @@ var ts; errors: [realizeDiagnostic(result.error, '\r\n')] }; } - var configFile = ts.parseConfigFile(result.config, _this.host, ts.getDirectoryPath(ts.normalizeSlashes(fileName))); + var configFile = ts.parseJsonConfigFileContent(result.config, _this.host, ts.getDirectoryPath(ts.normalizeSlashes(fileName))); return { options: configFile.options, files: configFile.fileNames,