From 1d619bcf604615dda9d0b15ba8ca5cc0bd2621e6 Mon Sep 17 00:00:00 2001 From: Taylor Date: Tue, 24 Mar 2026 02:46:12 -0700 Subject: [PATCH] fix: handle inspector session leak on Profiler.enable/start failure --- src/compiler/sys.ts | 3972 ++++++++++++++++++++++--------------------- 1 file changed, 1991 insertions(+), 1981 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c8f176603002b..42cd6ca6d7993 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1,1981 +1,1991 @@ -import { - AssertionLevel, - closeFileWatcher, - closeFileWatcherOf, - combinePaths, - Comparison, - contains, - containsPath, - createGetCanonicalFileName, - createMultiMap, - Debug, - directorySeparator, - emptyArray, - emptyFileSystemEntries, - endsWith, - enumerateInsertsAndDeletes, - FileSystemEntries, - getDirectoryPath, - getFallbackOptions, - getNormalizedAbsolutePath, - getRelativePathFromDirectory, - getRelativePathToDirectoryOrUrl, - getRootLength, - getStringComparer, - isArray, - isNodeLikeSystem, - isString, - mapDefined, - matchesExclude, - matchFiles, - memoize, - ModuleImportResult, - noop, - normalizePath, - normalizeSlashes, - orderedRemoveItem, - Path, - PollingWatchKind, - resolveJSModule, - some, - startsWith, - timestamp, - unorderedRemoveItem, - WatchDirectoryKind, - WatchFileKind, - WatchOptions, - writeFileEnsuringDirectories, -} from "./_namespaces/ts.js"; - -declare function setTimeout(handler: (...args: any[]) => void, timeout: number): any; -declare function clearTimeout(handle: any): void; - -/** - * djb2 hashing algorithm - * http://www.cse.yorku.ca/~oz/hash.html - * - * @internal - */ -export function generateDjb2Hash(data: string): string { - let acc = 5381; - for (let i = 0; i < data.length; i++) { - acc = ((acc << 5) + acc) + data.charCodeAt(i); - } - return acc.toString(); -} - -/** - * Set a high stack trace limit to provide more information in case of an error. - * Called for command-line and server use cases. - * Not called if TypeScript is used as a library. - * - * @internal - */ -export function setStackTraceLimit(): void { - if ((Error as any).stackTraceLimit < 100) { // Also tests that we won't set the property if it doesn't exist. - (Error as any).stackTraceLimit = 100; - } -} - -export enum FileWatcherEventKind { - Created, - Changed, - Deleted, -} - -export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, modifiedTime?: Date) => void; -export type DirectoryWatcherCallback = (fileName: string) => void; -interface WatchedFile { - readonly fileName: string; - readonly callback: FileWatcherCallback; - mtime: Date; -} - -/** @internal */ -export enum PollingInterval { - High = 2000, - Medium = 500, - Low = 250, -} - -/** @internal */ -export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) => FileWatcher; -/** @internal */ -export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined) => FileWatcher; - -/** @internal */ -export const missingFileModifiedTime: Date = new Date(0); // Any subsequent modification will occur after this time - -/** @internal */ -export function getModifiedTime(host: { getModifiedTime: NonNullable; }, fileName: string): Date { - return host.getModifiedTime(fileName) || missingFileModifiedTime; -} - -interface Levels { - Low: number; - Medium: number; - High: number; -} - -function createPollingIntervalBasedLevels(levels: Levels) { - return { - [PollingInterval.Low]: levels.Low, - [PollingInterval.Medium]: levels.Medium, - [PollingInterval.High]: levels.High, - }; -} - -const defaultChunkLevels: Levels = { Low: 32, Medium: 64, High: 256 }; -let pollingChunkSize = createPollingIntervalBasedLevels(defaultChunkLevels); -/** @internal */ -export let unchangedPollThresholds: { [K in PollingInterval]: number; } = createPollingIntervalBasedLevels(defaultChunkLevels); - -function setCustomPollingValues(system: System) { - if (!system.getEnvironmentVariable) { - return; - } - const pollingIntervalChanged = setCustomLevels("TSC_WATCH_POLLINGINTERVAL", PollingInterval); - pollingChunkSize = getCustomPollingBasedLevels("TSC_WATCH_POLLINGCHUNKSIZE", defaultChunkLevels) || pollingChunkSize; - unchangedPollThresholds = getCustomPollingBasedLevels("TSC_WATCH_UNCHANGEDPOLLTHRESHOLDS", defaultChunkLevels) || unchangedPollThresholds; - - function getLevel(envVar: string, level: keyof Levels) { - return system.getEnvironmentVariable(`${envVar}_${level.toUpperCase()}`); - } - - function getCustomLevels(baseVariable: string) { - let customLevels: Partial | undefined; - setCustomLevel("Low"); - setCustomLevel("Medium"); - setCustomLevel("High"); - return customLevels; - - function setCustomLevel(level: keyof Levels) { - const customLevel = getLevel(baseVariable, level); - if (customLevel) { - (customLevels || (customLevels = {}))[level] = Number(customLevel); - } - } - } - - function setCustomLevels(baseVariable: string, levels: Levels) { - const customLevels = getCustomLevels(baseVariable); - if (customLevels) { - setLevel("Low"); - setLevel("Medium"); - setLevel("High"); - return true; - } - return false; - - function setLevel(level: keyof Levels) { - levels[level] = customLevels![level] || levels[level]; - } - } - - function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { - const customLevels = getCustomLevels(baseVariable); - return (pollingIntervalChanged || customLevels) && - createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); - } -} - -interface WatchedFileWithIsClosed extends WatchedFile { - isClosed?: boolean; -} -function pollWatchedFileQueue( - host: { getModifiedTime: NonNullable; }, - queue: (T | undefined)[], - pollIndex: number, - chunkSize: number, - callbackOnWatchFileStat?: (watchedFile: T, pollIndex: number, fileChanged: boolean) => void, -) { - let definedValueCopyToIndex = pollIndex; - // Max visit would be all elements of the queue - for (let canVisit = queue.length; chunkSize && canVisit; nextPollIndex(), canVisit--) { - const watchedFile = queue[pollIndex]; - if (!watchedFile) { - continue; - } - else if (watchedFile.isClosed) { - queue[pollIndex] = undefined; - continue; - } - - // Only files polled count towards chunkSize - chunkSize--; - const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(host, watchedFile.fileName)); - if (watchedFile.isClosed) { - // Closed watcher as part of callback - queue[pollIndex] = undefined; - continue; - } - - callbackOnWatchFileStat?.(watchedFile, pollIndex, fileChanged); - // Defragment the queue while we are at it - if (queue[pollIndex]) { - // Copy this file to the non hole location - if (definedValueCopyToIndex < pollIndex) { - queue[definedValueCopyToIndex] = watchedFile; - queue[pollIndex] = undefined; - } - definedValueCopyToIndex++; - } - } - - // Return next poll index - return pollIndex; - - function nextPollIndex() { - pollIndex++; - if (pollIndex === queue.length) { - if (definedValueCopyToIndex < pollIndex) { - // There are holes from definedValueCopyToIndex to end of queue, change queue size - queue.length = definedValueCopyToIndex; - } - pollIndex = 0; - definedValueCopyToIndex = 0; - } - } -} - -interface WatchedFileWithUnchangedPolls extends WatchedFileWithIsClosed { - unchangedPolls: number; -} -function createDynamicPriorityPollingWatchFile(host: { - getModifiedTime: NonNullable; - setTimeout: NonNullable; -}): HostWatchFile { - interface PollingIntervalQueue extends Array { - pollingInterval: PollingInterval; - pollIndex: number; - pollScheduled: boolean; - } - - const watchedFiles: WatchedFileWithUnchangedPolls[] = []; - const changedFilesInLastPoll: WatchedFileWithUnchangedPolls[] = []; - const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low); - const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium); - const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High); - return watchFile; - - function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher { - const file: WatchedFileWithUnchangedPolls = { - fileName, - callback, - unchangedPolls: 0, - mtime: getModifiedTime(host, fileName), - }; - watchedFiles.push(file); - - addToPollingIntervalQueue(file, defaultPollingInterval); - return { - close: () => { - file.isClosed = true; - // Remove from watchedFiles - unorderedRemoveItem(watchedFiles, file); - // Do not update polling interval queue since that will happen as part of polling - }, - }; - } - - function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { - const queue = [] as WatchedFileWithUnchangedPolls[] as PollingIntervalQueue; - queue.pollingInterval = pollingInterval; - queue.pollIndex = 0; - queue.pollScheduled = false; - return queue; - } - - function pollPollingIntervalQueue(_timeoutType: string, queue: PollingIntervalQueue) { - queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, pollingChunkSize[queue.pollingInterval]); - // Set the next polling index and timeout - if (queue.length) { - scheduleNextPoll(queue.pollingInterval); - } - else { - Debug.assert(queue.pollIndex === 0); - queue.pollScheduled = false; - } - } - - function pollLowPollingIntervalQueue(_timeoutType: string, queue: PollingIntervalQueue) { - // Always poll complete list of changedFilesInLastPoll - pollQueue(changedFilesInLastPoll, PollingInterval.Low, /*pollIndex*/ 0, changedFilesInLastPoll.length); - - // Finally do the actual polling of the queue - pollPollingIntervalQueue(_timeoutType, queue); - // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue - // as pollPollingIntervalQueue wont schedule for next poll - if (!queue.pollScheduled && changedFilesInLastPoll.length) { - scheduleNextPoll(PollingInterval.Low); - } - } - - function pollQueue(queue: (WatchedFileWithUnchangedPolls | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) { - return pollWatchedFileQueue( - host, - queue, - pollIndex, - chunkSize, - onWatchFileStat, - ); - - function onWatchFileStat(watchedFile: WatchedFileWithUnchangedPolls, pollIndex: number, fileChanged: boolean) { - if (fileChanged) { - watchedFile.unchangedPolls = 0; - // Changed files go to changedFilesInLastPoll queue - if (queue !== changedFilesInLastPoll) { - queue[pollIndex] = undefined; - addChangedFileToLowPollingIntervalQueue(watchedFile); - } - } - else if (watchedFile.unchangedPolls !== unchangedPollThresholds[pollingInterval]) { - watchedFile.unchangedPolls++; - } - else if (queue === changedFilesInLastPoll) { - // Restart unchangedPollCount for unchanged file and move to low polling interval queue - watchedFile.unchangedPolls = 1; - queue[pollIndex] = undefined; - addToPollingIntervalQueue(watchedFile, PollingInterval.Low); - } - else if (pollingInterval !== PollingInterval.High) { - watchedFile.unchangedPolls++; - queue[pollIndex] = undefined; - addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High); - } - } - } - - function pollingIntervalQueue(pollingInterval: PollingInterval) { - switch (pollingInterval) { - case PollingInterval.Low: - return lowPollingIntervalQueue; - case PollingInterval.Medium: - return mediumPollingIntervalQueue; - case PollingInterval.High: - return highPollingIntervalQueue; - } - } - - function addToPollingIntervalQueue(file: WatchedFileWithUnchangedPolls, pollingInterval: PollingInterval) { - pollingIntervalQueue(pollingInterval).push(file); - scheduleNextPollIfNotAlreadyScheduled(pollingInterval); - } - - function addChangedFileToLowPollingIntervalQueue(file: WatchedFileWithUnchangedPolls) { - changedFilesInLastPoll.push(file); - scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); - } - - function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { - if (!pollingIntervalQueue(pollingInterval).pollScheduled) { - scheduleNextPoll(pollingInterval); - } - } - - function scheduleNextPoll(pollingInterval: PollingInterval) { - pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingInterval === PollingInterval.Low ? "pollLowPollingIntervalQueue" : "pollPollingIntervalQueue", pollingIntervalQueue(pollingInterval)); - } -} - -function createUseFsEventsOnParentDirectoryWatchFile( - fsWatch: FsWatch, - useCaseSensitiveFileNames: boolean, - getModifiedTime: NonNullable, - fsWatchWithTimestamp: boolean | undefined, -): HostWatchFile { - // One file can have multiple watchers - const fileWatcherCallbacks = createMultiMap(); - const fileTimestamps = fsWatchWithTimestamp ? new Map() : undefined; - const dirWatchers = new Map(); - const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); - return nonPollingWatchFile; - - function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { - const filePath = toCanonicalName(fileName); - if (fileWatcherCallbacks.add(filePath, callback).length === 1 && fileTimestamps) { - fileTimestamps.set(filePath, getModifiedTime(fileName) || missingFileModifiedTime); - } - const dirPath = getDirectoryPath(filePath) || "."; - const watcher = dirWatchers.get(dirPath) || - createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); - 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, fallbackOptions: WatchOptions | undefined) { - const watcher = fsWatch( - dirName, - FileSystemEntryKind.Directory, - (eventName: string, relativeFileName) => { - // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - if (!isString(relativeFileName)) return; - const fileName = getNormalizedAbsolutePath(relativeFileName, dirName); - const filePath = toCanonicalName(fileName); - // Some applications save a working file via rename operations - const callbacks = fileName && fileWatcherCallbacks.get(filePath); - if (callbacks) { - let currentModifiedTime; - let eventKind = FileWatcherEventKind.Changed; - if (fileTimestamps) { - const existingTime = fileTimestamps.get(filePath)!; - if (eventName === "change") { - currentModifiedTime = getModifiedTime(fileName) || missingFileModifiedTime; - if (currentModifiedTime.getTime() === existingTime.getTime()) return; - } - currentModifiedTime ||= getModifiedTime(fileName) || missingFileModifiedTime; - fileTimestamps.set(filePath, currentModifiedTime); - if (existingTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Created; - else if (currentModifiedTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Deleted; - } - for (const fileCallback of callbacks) { - fileCallback(fileName, eventKind, currentModifiedTime); - } - } - }, - /*recursive*/ false, - PollingInterval.Medium, - fallbackOptions, - ) as DirectoryWatcher; - watcher.referenceCount = 0; - dirWatchers.set(dirPath, watcher); - return watcher; - } -} - -function createFixedChunkSizePollingWatchFile(host: { - getModifiedTime: NonNullable; - setTimeout: NonNullable; -}): HostWatchFile { - const watchedFiles: (WatchedFileWithIsClosed | undefined)[] = []; - let pollIndex = 0; - let pollScheduled: any; - return watchFile; - - function watchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { - const file: WatchedFileWithIsClosed = { - fileName, - callback, - mtime: getModifiedTime(host, fileName), - }; - watchedFiles.push(file); - scheduleNextPoll(); - return { - close: () => { - file.isClosed = true; - unorderedRemoveItem(watchedFiles, file); - }, - }; - } - - function pollQueue() { - pollScheduled = undefined; - pollIndex = pollWatchedFileQueue(host, watchedFiles, pollIndex, pollingChunkSize[PollingInterval.Low]); - scheduleNextPoll(); - } - - function scheduleNextPoll() { - if (!watchedFiles.length || pollScheduled) return; - pollScheduled = host.setTimeout(pollQueue, PollingInterval.High, "pollQueue"); - } -} - -interface SingleFileWatcher { - watcher: FileWatcher; - callbacks: T[]; -} -function createSingleWatcherPerName( - cache: Map>, - useCaseSensitiveFileNames: boolean, - name: string, - callback: T, - createWatcher: (callback: T) => FileWatcher, -): FileWatcher { - const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - const path = toCanonicalFileName(name); - const existing = cache.get(path); - if (existing) { - existing.callbacks.push(callback); - } - else { - cache.set(path, { - watcher: createWatcher( - ( - // Cant infer types correctly so lets satisfy checker - (param1: any, param2: never, param3: any) => cache.get(path)?.callbacks.slice().forEach(cb => cb(param1, param2, param3)) - ) as T, - ), - callbacks: [callback], - }); - } - - return { - close: () => { - const watcher = cache.get(path); - // Watcher is not expected to be undefined, but if it is normally its because - // exception was thrown somewhere else and watch state is not what it should be - if (!watcher) return; - if (!orderedRemoveItem(watcher.callbacks, callback) || watcher.callbacks.length) return; - cache.delete(path); - closeFileWatcherOf(watcher); - }, - }; -} - -/** - * Returns true if file status changed - */ -function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean { - const oldTime = watchedFile.mtime.getTime(); - const newTime = modifiedTime.getTime(); - if (oldTime !== newTime) { - watchedFile.mtime = modifiedTime; - // Pass modified times so tsc --build can use it - watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime), modifiedTime); - return true; - } - - return false; -} - -/** @internal */ -export function getFileWatcherEventKind(oldTime: number, newTime: number): FileWatcherEventKind { - return oldTime === 0 - ? FileWatcherEventKind.Created - : newTime === 0 - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; -} - -/** @internal */ -export const ignoredPaths: readonly string[] = ["/node_modules/.", "/.git", "/.#"]; - -let curSysLog: (s: string) => void = noop; - -/** @internal */ -export function sysLog(s: string): void { - return curSysLog(s); -} - -/** @internal */ -export function setSysLog(logger: typeof sysLog): void { - curSysLog = logger; -} - -interface RecursiveDirectoryWatcherHost { - watchDirectory: HostWatchDirectory; - useCaseSensitiveFileNames: boolean; - getCurrentDirectory: System["getCurrentDirectory"]; - getAccessibleSortedChildDirectories(path: string): readonly string[]; - fileSystemEntryExists: FileSystemEntryExists; - realpath(s: string): string; - setTimeout: NonNullable; - clearTimeout: NonNullable; -} - -/** - * Watch the directory recursively using host provided method to watch child directories - * 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) - */ -function createDirectoryWatcherSupportingRecursive({ - watchDirectory, - useCaseSensitiveFileNames, - getCurrentDirectory, - getAccessibleSortedChildDirectories, - fileSystemEntryExists, - realpath, - setTimeout, - clearTimeout, -}: RecursiveDirectoryWatcherHost): HostWatchDirectory { - interface ChildDirectoryWatcher extends FileWatcher { - dirName: string; - } - type ChildWatches = readonly ChildDirectoryWatcher[]; - interface HostDirectoryWatcher { - watcher: FileWatcher; - childWatches: ChildWatches; - refCount: number; - targetWatcher: ChildDirectoryWatcher | undefined; - links: Set | undefined; - } - - const cache = new Map(); - const callbackCache = createMultiMap(); - const cacheToUpdateChildWatches = new Map(); - let timerToUpdateChildWatches: any; - - const filePathComparer = getStringComparer(!useCaseSensitiveFileNames); - const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames) as (fileName: string) => Path; - - return (dirName, callback, recursive, options) => - recursive ? - createDirectoryWatcher(dirName, options, callback) : - watchDirectory(dirName, callback, recursive, options); - - /** - * Create the directory watcher for the dirPath. - */ - function createDirectoryWatcher( - dirName: string, - options: WatchOptions | undefined, - callback?: DirectoryWatcherCallback, - link?: string, - ): ChildDirectoryWatcher { - const dirPath = toCanonicalFilePath(dirName); - let directoryWatcher = cache.get(dirPath); - if (directoryWatcher) { - directoryWatcher.refCount++; - } - else { - directoryWatcher = { - watcher: watchDirectory( - dirName, - fileName => { - if (isIgnoredPath(fileName, options)) return; - - if (options?.synchronousWatchDirectory) { - // Call the actual callback - if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, fileName); - - // Iterate through existing children and update the watches if needed - updateChildWatches(dirName, dirPath, options); - } - else { - nonSyncUpdateChildWatches(dirName, dirPath, fileName, options); - } - }, - /*recursive*/ false, - options, - ), - refCount: 1, - childWatches: emptyArray, - targetWatcher: undefined, - links: undefined, - }; - cache.set(dirPath, directoryWatcher); - updateChildWatches(dirName, dirPath, options); - } - - if (link) (directoryWatcher.links ??= new Set()).add(link); - - const callbackToAdd = callback && { dirName, callback }; - if (callbackToAdd) { - callbackCache.add(dirPath, callbackToAdd); - } - - return { - dirName, - close: () => { - const directoryWatcher = Debug.checkDefined(cache.get(dirPath)); - if (callbackToAdd) callbackCache.remove(dirPath, callbackToAdd); - if (link) directoryWatcher.links?.delete(link); - directoryWatcher.refCount--; - - if (directoryWatcher.refCount) return; - - cache.delete(dirPath); - directoryWatcher.links = undefined; - closeFileWatcherOf(directoryWatcher); - closeTargetWatcher(directoryWatcher); - directoryWatcher.childWatches.forEach(closeFileWatcher); - }, - }; - } - - type InvokeMap = Map; - function invokeCallbacks(dirName: string, dirPath: Path, fileName: string): void; - function invokeCallbacks(dirName: string, dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void; - function invokeCallbacks(dirName: string, dirPath: Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) { - let fileName: string | undefined; - let invokeMap: InvokeMap | undefined; - if (isString(fileNameOrInvokeMap)) { - fileName = fileNameOrInvokeMap; - } - else { - invokeMap = fileNameOrInvokeMap; - } - // Call the actual callback - callbackCache.forEach((callbacks, rootDirName) => { - if (invokeMap && invokeMap.get(rootDirName) === true) return; - if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) { - if (invokeMap) { - if (fileNames) { - const existing = invokeMap.get(rootDirName); - if (existing) { - (existing as string[]).push(...fileNames); - } - else { - invokeMap.set(rootDirName, fileNames.slice()); - } - } - else { - invokeMap.set(rootDirName, true); - } - } - else { - callbacks.forEach(({ callback }) => callback(fileName!)); - } - } - }); - cache.get(dirPath)?.links?.forEach(link => { - const toPathInLink = (fileName: string) => combinePaths(link, getRelativePathFromDirectory(dirName, fileName, toCanonicalFilePath)); - if (invokeMap) { - invokeCallbacks(link, toCanonicalFilePath(link), invokeMap, fileNames?.map(toPathInLink)); - } - else { - invokeCallbacks(link, toCanonicalFilePath(link), toPathInLink(fileName!)); - } - }); - } - - function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) { - // Iterate through existing children and update the watches if needed - const parentWatcher = cache.get(dirPath); - if (parentWatcher && fileSystemEntryExists(dirName, FileSystemEntryKind.Directory)) { - // Schedule the update and postpone invoke for callbacks - scheduleUpdateChildWatches(dirName, dirPath, fileName, options); - return; - } - - // Call the actual callbacks and remove child watches - invokeCallbacks(dirName, dirPath, fileName); - closeTargetWatcher(parentWatcher); - removeChildWatches(parentWatcher); - } - - function scheduleUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) { - const existing = cacheToUpdateChildWatches.get(dirPath); - if (existing) { - existing.fileNames.push(fileName); - } - else { - cacheToUpdateChildWatches.set(dirPath, { dirName, options, fileNames: [fileName] }); - } - if (timerToUpdateChildWatches) { - clearTimeout(timerToUpdateChildWatches); - timerToUpdateChildWatches = undefined; - } - timerToUpdateChildWatches = setTimeout(onTimerToUpdateChildWatches, 1000, "timerToUpdateChildWatches"); - } - - function onTimerToUpdateChildWatches() { - timerToUpdateChildWatches = undefined; - sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`); - const start = timestamp(); - const invokeMap = new Map(); - - while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) { - const result = cacheToUpdateChildWatches.entries().next(); - Debug.assert(!result.done); - const { value: [dirPath, { dirName, options, fileNames }] } = result; - cacheToUpdateChildWatches.delete(dirPath); - // Because the child refresh is fresh, we would need to invalidate whole root directory being watched - // to ensure that all the changes are reflected at this time - const hasChanges = updateChildWatches(dirName, dirPath, options); - if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, invokeMap, hasChanges ? undefined : fileNames); - } - - sysLog(`sysLog:: invokingWatchers:: Elapsed:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`); - callbackCache.forEach((callbacks, rootDirName) => { - const existing = invokeMap.get(rootDirName); - if (existing) { - callbacks.forEach(({ callback, dirName }) => { - if (isArray(existing)) { - existing.forEach(callback); - } - else { - callback(dirName); - } - }); - } - }); - - const elapsed = timestamp() - start; - sysLog(`sysLog:: Elapsed:: ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`); - } - - function removeChildWatches(parentWatcher: HostDirectoryWatcher | undefined) { - if (!parentWatcher) return; - const existingChildWatches = parentWatcher.childWatches; - parentWatcher.childWatches = emptyArray; - for (const childWatcher of existingChildWatches) { - childWatcher.close(); - removeChildWatches(cache.get(toCanonicalFilePath(childWatcher.dirName))); - } - } - - function closeTargetWatcher(watcher: HostDirectoryWatcher | undefined) { - if (watcher?.targetWatcher) { - watcher.targetWatcher.close(); - watcher.targetWatcher = undefined; - } - } - - function updateChildWatches(parentDir: string, parentDirPath: Path, options: WatchOptions | undefined) { - // Iterate through existing children and update the watches if needed - const parentWatcher = cache.get(parentDirPath); - if (!parentWatcher) return false; - const target = normalizePath(realpath(parentDir)); - let hasChanges; - let newChildWatches: ChildDirectoryWatcher[] | undefined; - if (filePathComparer(target, parentDir) === Comparison.EqualTo) { - // if (parentWatcher.target) closeFileWatcher - hasChanges = enumerateInsertsAndDeletes( - fileSystemEntryExists(parentDir, FileSystemEntryKind.Directory) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => { - const childFullName = getNormalizedAbsolutePath(child, parentDir); - // Filter our the symbolic link directories since those arent included in recursive watch - // which is same behaviour when recursive: true is passed to fs.watch - return !isIgnoredPath(childFullName, options) && filePathComparer(childFullName, normalizePath(realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined; - }) : emptyArray, - parentWatcher.childWatches, - (child, childWatcher) => filePathComparer(child, childWatcher.dirName), - createAndAddChildDirectoryWatcher, - closeFileWatcher, - addChildDirectoryWatcher, - ); - } - else if (parentWatcher.targetWatcher && filePathComparer(target, parentWatcher.targetWatcher.dirName) === Comparison.EqualTo) { - hasChanges = false; - Debug.assert(parentWatcher.childWatches === emptyArray); - } - else { - closeTargetWatcher(parentWatcher); - parentWatcher.targetWatcher = createDirectoryWatcher(target, options, /*callback*/ undefined, parentDir); - parentWatcher.childWatches.forEach(closeFileWatcher); - hasChanges = true; - } - parentWatcher.childWatches = newChildWatches || emptyArray; - return hasChanges; - - /** - * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list - */ - function createAndAddChildDirectoryWatcher(childName: string) { - const result = createDirectoryWatcher(childName, options); - addChildDirectoryWatcher(result); - } - - /** - * Add child directory watcher to the new ChildDirectoryWatcher list - */ - function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) { - (newChildWatches || (newChildWatches = [])).push(childWatcher); - } - } - - function isIgnoredPath(path: string, options: WatchOptions | undefined) { - return some(ignoredPaths, searchPath => isInPath(path, searchPath)) || - isIgnoredByWatchOptions(path, options, useCaseSensitiveFileNames, getCurrentDirectory); - } - - function isInPath(path: string, searchPath: string) { - if (path.includes(searchPath)) return true; - if (useCaseSensitiveFileNames) return false; - return toCanonicalFilePath(path).includes(searchPath); - } -} - -/** @internal */ -export type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined | null, modifiedTime?: Date) => void; // eslint-disable-line no-restricted-syntax -/** @internal */ -export type FsWatch = (fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined) => FileWatcher; -/** @internal */ -export interface FsWatchWorkerWatcher extends FileWatcher { - on(eventName: string, listener: () => void): void; -} -/** @internal */ -export type FsWatchWorker = (fileOrDirectory: string, recursive: boolean, callback: FsWatchCallback) => FsWatchWorkerWatcher; -/** @internal */ -export const enum FileSystemEntryKind { - File, - Directory, -} - -function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { - return (_fileName, eventKind, modifiedTime) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "", modifiedTime); -} - -function createFsWatchCallbackForFileWatcherCallback( - fileName: string, - callback: FileWatcherCallback, - getModifiedTime: NonNullable, -): FsWatchCallback { - return (eventName, _relativeFileName, modifiedTime) => { - if (eventName === "rename") { - // Check time stamps rather than file system entry checks - modifiedTime ||= getModifiedTime(fileName) || missingFileModifiedTime; - callback(fileName, modifiedTime !== missingFileModifiedTime ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted, modifiedTime); - } - else { - // Change - callback(fileName, FileWatcherEventKind.Changed, modifiedTime); - } - }; -} - -function isIgnoredByWatchOptions( - pathToCheck: string, - options: WatchOptions | undefined, - useCaseSensitiveFileNames: boolean, - getCurrentDirectory: System["getCurrentDirectory"], -) { - return (options?.excludeDirectories || options?.excludeFiles) && ( - matchesExclude(pathToCheck, options?.excludeFiles, useCaseSensitiveFileNames, getCurrentDirectory()) || - matchesExclude(pathToCheck, options?.excludeDirectories, useCaseSensitiveFileNames, getCurrentDirectory()) - ); -} - -function createFsWatchCallbackForDirectoryWatcherCallback( - directoryName: string, - callback: DirectoryWatcherCallback, - options: WatchOptions | undefined, - useCaseSensitiveFileNames: boolean, - getCurrentDirectory: System["getCurrentDirectory"], -): FsWatchCallback { - return (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 - const fileName = !relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName)); - if (!relativeFileName || !isIgnoredByWatchOptions(fileName, options, useCaseSensitiveFileNames, getCurrentDirectory)) { - callback(fileName); - } - } - }; -} - -/** @internal */ -export type FileSystemEntryExists = (fileorDirectrory: string, entryKind: FileSystemEntryKind) => boolean; - -/** @internal */ -export interface CreateSystemWatchFunctions { - // Polling watch file - pollingWatchFileWorker: HostWatchFile; - // For dynamic polling watch file - getModifiedTime: NonNullable; - setTimeout: NonNullable; - clearTimeout: NonNullable; - // For fs events : - fsWatchWorker: FsWatchWorker; - fileSystemEntryExists: FileSystemEntryExists; - useCaseSensitiveFileNames: boolean; - getCurrentDirectory: System["getCurrentDirectory"]; - fsSupportsRecursiveFsWatch: boolean; - getAccessibleSortedChildDirectories(path: string): readonly string[]; - realpath(s: string): string; - // For backward compatibility environment variables - tscWatchFile: string | undefined; - useNonPollingWatchers?: boolean; - tscWatchDirectory: string | undefined; - inodeWatching: boolean; - fsWatchWithTimestamp: boolean | undefined; - sysLog: (s: string) => void; -} - -/** @internal */ -export function createSystemWatchFunctions({ - pollingWatchFileWorker, - getModifiedTime, - setTimeout, - clearTimeout, - fsWatchWorker, - fileSystemEntryExists, - useCaseSensitiveFileNames, - getCurrentDirectory, - fsSupportsRecursiveFsWatch, - getAccessibleSortedChildDirectories, - realpath, - tscWatchFile, - useNonPollingWatchers, - tscWatchDirectory, - inodeWatching, - fsWatchWithTimestamp, - sysLog, -}: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } { - const pollingWatches = new Map>(); - const fsWatches = new Map>(); - const fsWatchesRecursive = new Map>(); - let dynamicPollingWatchFile: HostWatchFile | undefined; - let fixedChunkSizePollingWatchFile: HostWatchFile | undefined; - let nonPollingWatchFile: HostWatchFile | undefined; - let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined; - let hitSystemWatcherLimit = false; - return { - watchFile, - watchDirectory, - }; - - function watchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined): FileWatcher { - options = updateOptionsForWatchFile(options, useNonPollingWatchers); - const watchFileKind = Debug.checkDefined(options.watchFile); - switch (watchFileKind) { - case WatchFileKind.FixedPollingInterval: - return pollingWatchFile(fileName, callback, PollingInterval.Low, /*options*/ undefined); - case WatchFileKind.PriorityPollingInterval: - return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined); - case WatchFileKind.DynamicPriorityPolling: - return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined); - case WatchFileKind.FixedChunkSizePolling: - return ensureFixedChunkSizePollingWatchFile()(fileName, callback, /* pollingInterval */ undefined!, /*options*/ undefined); - case WatchFileKind.UseFsEvents: - return fsWatch( - fileName, - FileSystemEntryKind.File, - createFsWatchCallbackForFileWatcherCallback(fileName, callback, getModifiedTime), - /*recursive*/ false, - pollingInterval, - getFallbackOptions(options), - ); - case WatchFileKind.UseFsEventsOnParentDirectory: - if (!nonPollingWatchFile) { - nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames, getModifiedTime, fsWatchWithTimestamp); - } - return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options)); - default: - Debug.assertNever(watchFileKind); - } - } - - function ensureDynamicPollingWatchFile() { - return dynamicPollingWatchFile ||= createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); - } - - function ensureFixedChunkSizePollingWatchFile() { - return fixedChunkSizePollingWatchFile ||= createFixedChunkSizePollingWatchFile({ getModifiedTime, setTimeout }); - } - - function updateOptionsForWatchFile(options: WatchOptions | undefined, useNonPollingWatchers?: boolean): WatchOptions { - if (options && options.watchFile !== undefined) return options; - switch (tscWatchFile) { - case "PriorityPollingInterval": - // Use polling interval based on priority when create watch using host.watchFile - return { watchFile: WatchFileKind.PriorityPollingInterval }; - case "DynamicPriorityPolling": - // Use polling interval but change the interval depending on file changes and their default polling interval - return { watchFile: WatchFileKind.DynamicPriorityPolling }; - case "UseFsEvents": - // Use notifications from FS to watch with falling back to fs.watchFile - return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.PriorityInterval, options); - case "UseFsEventsWithFallbackDynamicPolling": - // Use notifications from FS to watch with falling back to dynamic watch file - return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.DynamicPriority, options); - case "UseFsEventsOnParentDirectory": - useNonPollingWatchers = true; - // fall through - default: - return useNonPollingWatchers ? - // Use notifications from FS to watch with falling back to fs.watchFile - generateWatchFileOptions(WatchFileKind.UseFsEventsOnParentDirectory, PollingWatchKind.PriorityInterval, options) : - // Default to using fs events - { watchFile: WatchFileKind.UseFsEvents }; - } - } - - function generateWatchFileOptions( - watchFile: WatchFileKind, - fallbackPolling: PollingWatchKind, - options: WatchOptions | undefined, - ): WatchOptions { - const defaultFallbackPolling = options?.fallbackPolling; - return { - watchFile, - fallbackPolling: defaultFallbackPolling === undefined ? - fallbackPolling : - defaultFallbackPolling, - }; - } - - function watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { - if (fsSupportsRecursiveFsWatch) { - return fsWatch( - directoryName, - FileSystemEntryKind.Directory, - createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), - recursive, - PollingInterval.Medium, - getFallbackOptions(options), - ); - } - - if (!hostRecursiveDirectoryWatcher) { - hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({ - useCaseSensitiveFileNames, - getCurrentDirectory, - fileSystemEntryExists, - getAccessibleSortedChildDirectories, - watchDirectory: nonRecursiveWatchDirectory, - realpath, - setTimeout, - clearTimeout, - }); - } - return hostRecursiveDirectoryWatcher(directoryName, callback, recursive, options); - } - - function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { - Debug.assert(!recursive); - const watchDirectoryOptions = updateOptionsForWatchDirectory(options); - const watchDirectoryKind = Debug.checkDefined(watchDirectoryOptions.watchDirectory); - switch (watchDirectoryKind) { - case WatchDirectoryKind.FixedPollingInterval: - return pollingWatchFile( - directoryName, - () => callback(directoryName), - PollingInterval.Medium, - /*options*/ undefined, - ); - case WatchDirectoryKind.DynamicPriorityPolling: - return ensureDynamicPollingWatchFile()( - directoryName, - () => callback(directoryName), - PollingInterval.Medium, - /*options*/ undefined, - ); - case WatchDirectoryKind.FixedChunkSizePolling: - return ensureFixedChunkSizePollingWatchFile()( - directoryName, - () => callback(directoryName), - /* pollingInterval */ undefined!, - /*options*/ undefined, - ); - case WatchDirectoryKind.UseFsEvents: - return fsWatch( - directoryName, - FileSystemEntryKind.Directory, - createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), - recursive, - PollingInterval.Medium, - getFallbackOptions(watchDirectoryOptions), - ); - default: - Debug.assertNever(watchDirectoryKind); - } - } - - function updateOptionsForWatchDirectory(options: WatchOptions | undefined): WatchOptions { - if (options && options.watchDirectory !== undefined) return options; - switch (tscWatchDirectory) { - case "RecursiveDirectoryUsingFsWatchFile": - // Use polling interval based on priority when create watch using host.watchFile - return { watchDirectory: WatchDirectoryKind.FixedPollingInterval }; - case "RecursiveDirectoryUsingDynamicPriorityPolling": - // Use polling interval but change the interval depending on file changes and their default polling interval - return { watchDirectory: WatchDirectoryKind.DynamicPriorityPolling }; - default: - const defaultFallbackPolling = options?.fallbackPolling; - return { - watchDirectory: WatchDirectoryKind.UseFsEvents, - fallbackPolling: defaultFallbackPolling !== undefined ? - defaultFallbackPolling : - undefined, - }; - } - } - - function pollingWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) { - return createSingleWatcherPerName( - pollingWatches, - useCaseSensitiveFileNames, - fileName, - callback, - cb => pollingWatchFileWorker(fileName, cb, pollingInterval, options), - ); - } - function fsWatch( - fileOrDirectory: string, - entryKind: FileSystemEntryKind, - callback: FsWatchCallback, - recursive: boolean, - fallbackPollingInterval: PollingInterval, - fallbackOptions: WatchOptions | undefined, - ): FileWatcher { - return createSingleWatcherPerName( - recursive ? fsWatchesRecursive : fsWatches, - useCaseSensitiveFileNames, - fileOrDirectory, - callback, - cb => fsWatchHandlingExistenceOnHost(fileOrDirectory, entryKind, cb, recursive, fallbackPollingInterval, fallbackOptions), - ); - } - - function fsWatchHandlingExistenceOnHost( - fileOrDirectory: string, - entryKind: FileSystemEntryKind, - callback: FsWatchCallback, - recursive: boolean, - fallbackPollingInterval: PollingInterval, - fallbackOptions: WatchOptions | undefined, - ): FileWatcher { - let lastDirectoryPartWithDirectorySeparator: string | undefined; - let lastDirectoryPart: string | undefined; - if (inodeWatching) { - lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substring(fileOrDirectory.lastIndexOf(directorySeparator)); - lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(directorySeparator.length); - } - /** Watcher for the file system entry depending on whether it is missing or present */ - let watcher: FileWatcher | undefined = !fileSystemEntryExists(fileOrDirectory, entryKind) ? - watchMissingFileSystemEntry() : - watchPresentFileSystemEntry(); - return { - close: () => { - // Close the watcher (either existing file system entry watcher or missing file system entry watcher) - if (watcher) { - watcher.close(); - watcher = undefined; - } - }, - }; - - function updateWatcher(createWatcher: () => FileWatcher) { - // If watcher is not closed, update it - if (watcher) { - sysLog(`sysLog:: ${fileOrDirectory}:: Changing watcher to ${createWatcher === watchPresentFileSystemEntry ? "Present" : "Missing"}FileSystemEntryWatcher`); - watcher.close(); - watcher = createWatcher(); - } - } - - /** - * Watch the file or directory that is currently present - * and when the watched file or directory is deleted, switch to missing file system entry watcher - */ - function watchPresentFileSystemEntry(): FileWatcher { - if (hitSystemWatcherLimit) { - sysLog(`sysLog:: ${fileOrDirectory}:: Defaulting to watchFile`); - return watchPresentFileSystemEntryWithFsWatchFile(); - } - try { - const presentWatcher = (entryKind === FileSystemEntryKind.Directory || !fsWatchWithTimestamp ? fsWatchWorker : fsWatchWorkerHandlingTimestamp)( - fileOrDirectory, - recursive, - inodeWatching ? - callbackChangingToMissingFileSystemEntry : - callback, - ); - // Watch the missing file or directory or error - presentWatcher.on("error", () => { - callback("rename", ""); - updateWatcher(watchMissingFileSystemEntry); - }); - return presentWatcher; - } - 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 fs.watchFile - hitSystemWatcherLimit ||= e.code === "ENOSPC"; - sysLog(`sysLog:: ${fileOrDirectory}:: Changing to watchFile`); - return watchPresentFileSystemEntryWithFsWatchFile(); - } - } - - function callbackChangingToMissingFileSystemEntry(event: "rename" | "change", relativeName: string | undefined | null) { // eslint-disable-line no-restricted-syntax - // In some scenarios, file save operation fires event with fileName.ext~ instead of fileName.ext - // To ensure we see the file going missing and coming back up (file delete and then recreated) - // and watches being updated correctly we are calling back with fileName.ext as well as fileName.ext~ - // The worst is we have fired event that was not needed but we wont miss any changes - // especially in cases where file goes missing and watches wrong inode - let originalRelativeName: string | undefined; - if (relativeName && endsWith(relativeName, "~")) { - originalRelativeName = relativeName; - relativeName = relativeName.slice(0, relativeName.length - 1); - } - // because relativeName is not guaranteed to be correct we need to check on each rename with few combinations - // Eg on ubuntu while watching app/node_modules the relativeName is "node_modules" which is neither relative nor full path - if ( - event === "rename" && - (!relativeName || - relativeName === lastDirectoryPart || - endsWith(relativeName, lastDirectoryPartWithDirectorySeparator!)) - ) { - const modifiedTime = getModifiedTime(fileOrDirectory) || missingFileModifiedTime; - if (originalRelativeName) callback(event, originalRelativeName, modifiedTime); - callback(event, relativeName, modifiedTime); - if (inodeWatching) { - // If this was rename event, inode has changed means we need to update watcher - updateWatcher(modifiedTime === missingFileModifiedTime ? watchMissingFileSystemEntry : watchPresentFileSystemEntry); - } - else if (modifiedTime === missingFileModifiedTime) { - updateWatcher(watchMissingFileSystemEntry); - } - } - else { - if (originalRelativeName) callback(event, originalRelativeName); - callback(event, relativeName); - } - } - - /** - * Watch the file or directory using fs.watchFile since fs.watch threw exception - * 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 - */ - function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { - return watchFile( - fileOrDirectory, - createFileWatcherCallback(callback), - fallbackPollingInterval, - fallbackOptions, - ); - } - - /** - * Watch the file or directory that is missing - * and switch to existing file or directory when the missing filesystem entry is created - */ - function watchMissingFileSystemEntry(): FileWatcher { - return watchFile( - fileOrDirectory, - (_fileName, eventKind, modifiedTime) => { - if (eventKind === FileWatcherEventKind.Created) { - modifiedTime ||= getModifiedTime(fileOrDirectory) || missingFileModifiedTime; - if (modifiedTime !== missingFileModifiedTime) { - callback("rename", "", modifiedTime); - // Call the callback for current file or directory - // For now it could be callback for the inner directory creation, - // but just return current directory, better than current no-op - updateWatcher(watchPresentFileSystemEntry); - } - } - }, - fallbackPollingInterval, - fallbackOptions, - ); - } - } - - function fsWatchWorkerHandlingTimestamp(fileOrDirectory: string, recursive: boolean, callback: FsWatchCallback): FsWatchWorkerWatcher { - let modifiedTime = getModifiedTime(fileOrDirectory) || missingFileModifiedTime; - return fsWatchWorker(fileOrDirectory, recursive, (eventName, relativeFileName, currentModifiedTime) => { - if (eventName === "change") { - currentModifiedTime ||= getModifiedTime(fileOrDirectory) || missingFileModifiedTime; - if (currentModifiedTime.getTime() === modifiedTime.getTime()) return; - } - modifiedTime = currentModifiedTime || getModifiedTime(fileOrDirectory) || missingFileModifiedTime; - callback(eventName, relativeFileName, modifiedTime); - }); - } -} - -/** - * patch writefile to create folder before writing the file - * - * @internal - */ -export function patchWriteFileEnsuringDirectory(sys: System): void { - // patch writefile to create folder before writing the file - const originalWriteFile = sys.writeFile; - sys.writeFile = (path, data, writeBom) => - writeFileEnsuringDirectories( - path, - data, - !!writeBom, - (path, data, writeByteOrderMark) => originalWriteFile.call(sys, path, data, writeByteOrderMark), - path => sys.createDirectory(path), - path => sys.directoryExists(path), - ); -} - -export type BufferEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"; - -// TODO: GH#18217 Methods on System are often used as if they are certainly defined -export interface System { - args: string[]; - newLine: string; - useCaseSensitiveFileNames: boolean; - write(s: string): void; - writeOutputIsTTY?(): boolean; - getWidthOfTerminal?(): number; - readFile(path: string, encoding?: string): string | undefined; - getFileSize?(path: string): number; - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - - /** - * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that - * use native OS file watching - */ - watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher; - watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher; - /**@internal */ preferNonRecursiveWatch?: boolean; - resolvePath(path: string): string; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; - getExecutingFilePath(): string; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[]; - getModifiedTime?(path: string): Date | undefined; - setModifiedTime?(path: string, time: Date): void; - deleteFile?(path: string): void; - /** - * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) - */ - createHash?(data: string): string; - /** This must be cryptographically secure. Only implement this method using `crypto.createHash("sha256")`. */ - createSHA256Hash?(data: string): string; - getMemoryUsage?(): number; - exit(exitCode?: number): void; - /** @internal */ enableCPUProfiler?(path: string, continuation: () => void): boolean; - /** @internal */ disableCPUProfiler?(continuation: () => void): boolean; - /** @internal */ cpuProfilingEnabled?(): boolean; - realpath?(path: string): string; - /** @internal */ getEnvironmentVariable(name: string): string; - /** @internal */ tryEnableSourceMapsForHost?(): void; - /** @internal */ getAccessibleFileSystemEntries?(path: string): FileSystemEntries; - /** @internal */ debugMode?: boolean; - setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; - clearTimeout?(timeoutId: any): void; - clearScreen?(): void; - /** @internal */ setBlocking?(): void; - base64decode?(input: string): string; - base64encode?(input: string): string; - /** @internal */ require?(baseDir: string, moduleName: string): ModuleImportResult; - - // For testing - /** @internal */ now?(): Date; - /** @internal */ storeSignatureInfo?: boolean; -} - -export interface FileWatcher { - close(): void; -} - -interface DirectoryWatcher extends FileWatcher { - referenceCount: number; -} - -// TODO: GH#18217 this is used as if it's certainly defined in many places. -export let sys: System = (() => { - // NodeJS detects "\uFEFF" at the start of the string and *replaces* it with the actual - // byte order mark from the specified encoding. Using any other byte order mark does - // not actually work. - const byteOrderMarkIndicator = "\uFEFF"; - - function getNodeSystem(): System { - const nativePattern = /^native |^\([^)]+\)$|^(?:internal[\\/]|[\w\s]+(?:\.js)?$)/; - const _fs: typeof import("fs") = require("fs"); - const _path: typeof import("path") = require("path"); - const _os = require("os"); - // crypto can be absent on reduced node installations - let _crypto: typeof import("crypto") | undefined; - try { - _crypto = require("crypto"); - } - catch { - _crypto = undefined; - } - let activeSession: import("inspector").Session | "stopping" | undefined; - let profilePath = "./profile.cpuprofile"; - - const isMacOs = process.platform === "darwin"; - const isLinuxOrMacOs = process.platform === "linux" || isMacOs; - - const statSyncOptions = { throwIfNoEntry: false } as const; - - const platform: string = _os.platform(); - const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); - const fsRealpath = !!_fs.realpathSync.native ? process.platform === "win32" ? fsRealPathHandlingLongPath : _fs.realpathSync.native : _fs.realpathSync; - - // If our filename is "sys.js", then we are executing unbundled on the raw tsc output. - // In that case, simulate a faked path in the directory where a bundle would normally - // appear (e.g. the directory containing lib.*.d.ts files). - // - // Note that if we ever emit as files like cjs/mjs, this check will be wrong. - const executingFilePath = __filename.endsWith("sys.js") ? _path.join(_path.dirname(__dirname), "__fake__.js") : __filename; - - const fsSupportsRecursiveFsWatch = process.platform === "win32" || isMacOs; - const getCurrentDirectory = memoize(() => process.cwd()); - const { watchFile, watchDirectory } = createSystemWatchFunctions({ - pollingWatchFileWorker: fsWatchFileWorker, - getModifiedTime, - setTimeout, - clearTimeout, - fsWatchWorker, - useCaseSensitiveFileNames, - getCurrentDirectory, - fileSystemEntryExists, - // 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) - fsSupportsRecursiveFsWatch, - getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, - realpath, - tscWatchFile: process.env.TSC_WATCHFILE, - useNonPollingWatchers: !!process.env.TSC_NONPOLLING_WATCHER, - tscWatchDirectory: process.env.TSC_WATCHDIRECTORY, - inodeWatching: isLinuxOrMacOs, - fsWatchWithTimestamp: isMacOs, - sysLog, - }); - const nodeSystem: System = { - args: process.argv.slice(2), - newLine: _os.EOL, - useCaseSensitiveFileNames, - write(s: string): void { - process.stdout.write(s); - }, - getWidthOfTerminal() { - return process.stdout.columns; - }, - writeOutputIsTTY() { - return process.stdout.isTTY; - }, - readFile, - writeFile, - watchFile, - watchDirectory, - preferNonRecursiveWatch: !fsSupportsRecursiveFsWatch, - resolvePath: path => _path.resolve(path), - fileExists, - directoryExists, - getAccessibleFileSystemEntries, - createDirectory(directoryName: string) { - if (!nodeSystem.directoryExists(directoryName)) { - // Wrapped in a try-catch to prevent crashing if we are in a race - // with another copy of ourselves to create the same directory - try { - _fs.mkdirSync(directoryName); - } - catch (e) { - if (e.code !== "EEXIST") { - // Failed for some other reason (access denied?); still throw - throw e; - } - } - } - }, - getExecutingFilePath() { - return executingFilePath; - }, - getCurrentDirectory, - getDirectories, - getEnvironmentVariable(name: string) { - return process.env[name] || ""; - }, - readDirectory, - getModifiedTime, - setModifiedTime, - deleteFile, - createHash: _crypto ? createSHA256Hash : generateDjb2Hash, - createSHA256Hash: _crypto ? createSHA256Hash : undefined, - getMemoryUsage() { - if (global.gc) { - global.gc(); - } - return process.memoryUsage().heapUsed; - }, - getFileSize(path) { - const stat = statSync(path); - if (stat?.isFile()) { - return stat.size; - } - return 0; - }, - exit(exitCode?: number): void { - disableCPUProfiler(() => process.exit(exitCode)); - }, - enableCPUProfiler, - disableCPUProfiler, - cpuProfilingEnabled: () => !!activeSession || contains(process.execArgv, "--cpu-prof") || contains(process.execArgv, "--prof"), - realpath, - debugMode: !!process.env.NODE_INSPECTOR_IPC || !!process.env.VSCODE_INSPECTOR_OPTIONS || some(process.execArgv, arg => /^--(?:inspect|debug)(?:-brk)?(?:=\d+)?$/i.test(arg)) || !!(process as any).recordreplay, - tryEnableSourceMapsForHost() { - try { - (require("source-map-support") as typeof import("source-map-support")).install(); - } - catch { - // Could not enable source maps. - } - }, - setTimeout, - clearTimeout, - clearScreen: () => { - process.stdout.write("\x1B[2J\x1B[3J\x1B[H"); - }, - setBlocking: () => { - const handle = (process.stdout as any)?._handle as { setBlocking?: (value: boolean) => void; }; - if (handle && handle.setBlocking) { - handle.setBlocking(true); - } - }, - base64decode: input => Buffer.from(input, "base64").toString("utf8"), - base64encode: input => Buffer.from(input).toString("base64"), - require: (baseDir, moduleName) => { - try { - const modulePath = resolveJSModule(moduleName, baseDir, nodeSystem); - return { module: require(modulePath), modulePath, error: undefined }; - } - catch (error) { - return { module: undefined, modulePath: undefined, error }; - } - }, - }; - return nodeSystem; - - /** Calls fs.statSync, returning undefined if any errors are thrown */ - function statSync(path: string): import("fs").Stats | undefined { - // throwIfNoEntry is available in Node 14.17 and above, which matches our supported range. - try { - return _fs.statSync(path, statSyncOptions); - } - catch { - // This should never happen as we are passing throwIfNoEntry: false, - // but guard against this just in case (e.g. a polyfill doesn't check this flag). - return undefined; - } - } - - /** - * Uses the builtin inspector APIs to capture a CPU profile - * See https://nodejs.org/api/inspector.html#inspector_example_usage for details - */ - function enableCPUProfiler(path: string, cb: () => void) { - if (activeSession) { - cb(); - return false; - } - const inspector: typeof import("inspector") = require("inspector"); - if (!inspector || !inspector.Session) { - cb(); - return false; - } - const session = new inspector.Session(); - session.connect(); - - session.post("Profiler.enable", () => { - session.post("Profiler.start", () => { - activeSession = session; - profilePath = path; - cb(); - }); - }); - return true; - } - - /** - * Strips non-TS paths from the profile, so users with private projects shouldn't - * need to worry about leaking paths by submitting a cpu profile to us - */ - function cleanupPaths(profile: import("inspector").Profiler.Profile) { - let externalFileCounter = 0; - const remappedPaths = new Map(); - const normalizedDir = normalizeSlashes(_path.dirname(executingFilePath)); - // Windows rooted dir names need an extra `/` prepended to be valid file:/// urls - const fileUrlRoot = `file://${getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`; - for (const node of profile.nodes) { - if (node.callFrame.url) { - const url = normalizeSlashes(node.callFrame.url); - if (containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) { - node.callFrame.url = getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, createGetCanonicalFileName(useCaseSensitiveFileNames), /*isAbsolutePathAnUrl*/ true); - } - else if (!nativePattern.test(url)) { - node.callFrame.url = (remappedPaths.has(url) ? remappedPaths : remappedPaths.set(url, `external${externalFileCounter}.js`)).get(url)!; - externalFileCounter++; - } - } - } - return profile; - } - - function disableCPUProfiler(cb: () => void) { - if (activeSession && activeSession !== "stopping") { - const s = activeSession; - activeSession.post("Profiler.stop", (err, { profile }) => { - if (!err) { - if (statSync(profilePath)?.isDirectory()) { - profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`); - } - try { - _fs.mkdirSync(_path.dirname(profilePath), { recursive: true }); - } - catch { - // do nothing and ignore fallible fs operation - } - _fs.writeFileSync(profilePath, JSON.stringify(cleanupPaths(profile))); - } - activeSession = undefined; - s.disconnect(); - cb(); - }); - activeSession = "stopping"; - return true; - } - else { - cb(); - return false; - } - } - - function isFileSystemCaseSensitive(): boolean { - // win32\win64 are case insensitive platforms - if (platform === "win32" || platform === "win64") { - return false; - } - // If this file exists under a different case, we must be case-insensitve. - return !fileExists(swapCase(__filename)); - } - - /** Convert all lowercase chars to uppercase, and vice-versa */ - function swapCase(s: string): string { - return s.replace(/\w/g, ch => { - const up = ch.toUpperCase(); - return ch === up ? ch.toLowerCase() : up; - }); - } - - function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval: number): FileWatcher { - _fs.watchFile(fileName, { persistent: true, interval: pollingInterval }, fileChanged); - let eventKind: FileWatcherEventKind; - return { - close: () => _fs.unwatchFile(fileName, fileChanged), - }; - - function fileChanged(curr: import("fs").Stats, prev: import("fs").Stats) { - // previous event kind check is to ensure we recongnize the file as previously also missing when it is restored or renamed twice (that is it disappears and reappears) - // In such case, prevTime returned is same as prev time of event when file was deleted as per node documentation - const isPreviouslyDeleted = +prev.mtime === 0 || eventKind === FileWatcherEventKind.Deleted; - if (+curr.mtime === 0) { - if (isPreviouslyDeleted) { - // Already deleted file, no need to callback again - return; - } - eventKind = FileWatcherEventKind.Deleted; - } - else if (isPreviouslyDeleted) { - 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, curr.mtime); - } - } - - function fsWatchWorker( - fileOrDirectory: string, - recursive: boolean, - callback: FsWatchCallback, - ) { - // 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( - fileOrDirectory, - fsSupportsRecursiveFsWatch ? - { persistent: true, recursive: !!recursive } : { persistent: true }, - callback, - ); - } - - function readFile(fileName: string, _encoding?: string): string | undefined { - let buffer: Buffer; - try { - buffer = _fs.readFileSync(fileName); - } - catch { - return undefined; - } - let len = buffer.length; - if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) { - // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js, - // flip all byte pairs and treat as little endian. - len &= ~1; // Round down to a multiple of 2 - for (let i = 0; i < len; i += 2) { - const temp = buffer[i]; - buffer[i] = buffer[i + 1]; - buffer[i + 1] = temp; - } - return buffer.toString("utf16le", 2); - } - if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { - // Little endian UTF-16 byte order mark detected - return buffer.toString("utf16le", 2); - } - if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { - // UTF-8 byte order mark detected - return buffer.toString("utf8", 3); - } - // Default is UTF-8 with no byte order mark - return buffer.toString("utf8"); - } - - function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { - // If a BOM is required, emit one - if (writeByteOrderMark) { - data = byteOrderMarkIndicator + data; - } - - let fd: number | undefined; - - try { - fd = _fs.openSync(fileName, "w"); - _fs.writeSync(fd, data, /*position*/ undefined, "utf8"); - } - finally { - if (fd !== undefined) { - _fs.closeSync(fd); - } - } - } - - function getAccessibleFileSystemEntries(path: string): FileSystemEntries { - try { - const entries = _fs.readdirSync(path || ".", { withFileTypes: true }); - const files: string[] = []; - const directories: string[] = []; - for (const dirent of entries) { - // withFileTypes is not supported before Node 10.10. - const entry = typeof dirent === "string" ? dirent : dirent.name; - - // This is necessary because on some file system node fails to exclude - // "." and "..". See https://github.com/nodejs/node/issues/4002 - if (entry === "." || entry === "..") { - continue; - } - - let stat: any; - if (typeof dirent === "string" || dirent.isSymbolicLink()) { - const name = combinePaths(path, entry); - - stat = statSync(name); - if (!stat) { - continue; - } - } - else { - stat = dirent; - } - - if (stat.isFile()) { - files.push(entry); - } - else if (stat.isDirectory()) { - directories.push(entry); - } - } - files.sort(); - directories.sort(); - return { files, directories }; - } - catch { - return emptyFileSystemEntries; - } - } - - function readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] { - return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries, realpath); - } - - function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { - const stat = statSync(path); - if (!stat) { - return false; - } - switch (entryKind) { - case FileSystemEntryKind.File: - return stat.isFile(); - case FileSystemEntryKind.Directory: - return stat.isDirectory(); - default: - return false; - } - } - - function fileExists(path: string): boolean { - return fileSystemEntryExists(path, FileSystemEntryKind.File); - } - - function directoryExists(path: string): boolean { - return fileSystemEntryExists(path, FileSystemEntryKind.Directory); - } - - function getDirectories(path: string): string[] { - return getAccessibleFileSystemEntries(path).directories.slice(); - } - - function fsRealPathHandlingLongPath(path: string): string { - return path.length < 260 ? _fs.realpathSync.native(path) : _fs.realpathSync(path); - } - - function realpath(path: string): string { - try { - return fsRealpath(path); - } - catch { - return path; - } - } - - function getModifiedTime(path: string) { - return statSync(path)?.mtime; - } - - function setModifiedTime(path: string, time: Date) { - try { - _fs.utimesSync(path, time, time); - } - catch { - return; - } - } - - function deleteFile(path: string) { - try { - return _fs.unlinkSync(path); - } - catch { - return; - } - } - - function createSHA256Hash(data: string): string { - const hash = _crypto!.createHash("sha256"); - hash.update(data); - return hash.digest("hex"); - } - } - - let sys: System | undefined; - if (isNodeLikeSystem()) { - sys = getNodeSystem(); - } - if (sys) { - // patch writefile to create folder before writing the file - patchWriteFileEnsuringDirectory(sys); - } - return sys!; -})(); - -/** @internal @knipignore */ -export function setSys(s: System): void { - sys = s; -} - -if (sys && sys.getEnvironmentVariable) { - setCustomPollingValues(sys); - Debug.setAssertionLevel( - /^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) - ? AssertionLevel.Normal - : AssertionLevel.None, - ); -} -if (sys && sys.debugMode) { - Debug.isDebugging = true; -} +import { + AssertionLevel, + closeFileWatcher, + closeFileWatcherOf, + combinePaths, + Comparison, + contains, + containsPath, + createGetCanonicalFileName, + createMultiMap, + Debug, + directorySeparator, + emptyArray, + emptyFileSystemEntries, + endsWith, + enumerateInsertsAndDeletes, + FileSystemEntries, + getDirectoryPath, + getFallbackOptions, + getNormalizedAbsolutePath, + getRelativePathFromDirectory, + getRelativePathToDirectoryOrUrl, + getRootLength, + getStringComparer, + isArray, + isNodeLikeSystem, + isString, + mapDefined, + matchesExclude, + matchFiles, + memoize, + ModuleImportResult, + noop, + normalizePath, + normalizeSlashes, + orderedRemoveItem, + Path, + PollingWatchKind, + resolveJSModule, + some, + startsWith, + timestamp, + unorderedRemoveItem, + WatchDirectoryKind, + WatchFileKind, + WatchOptions, + writeFileEnsuringDirectories, +} from "./_namespaces/ts.js"; + +declare function setTimeout(handler: (...args: any[]) => void, timeout: number): any; +declare function clearTimeout(handle: any): void; + +/** + * djb2 hashing algorithm + * http://www.cse.yorku.ca/~oz/hash.html + * + * @internal + */ +export function generateDjb2Hash(data: string): string { + let acc = 5381; + for (let i = 0; i < data.length; i++) { + acc = ((acc << 5) + acc) + data.charCodeAt(i); + } + return acc.toString(); +} + +/** + * Set a high stack trace limit to provide more information in case of an error. + * Called for command-line and server use cases. + * Not called if TypeScript is used as a library. + * + * @internal + */ +export function setStackTraceLimit(): void { + if ((Error as any).stackTraceLimit < 100) { // Also tests that we won't set the property if it doesn't exist. + (Error as any).stackTraceLimit = 100; + } +} + +export enum FileWatcherEventKind { + Created, + Changed, + Deleted, +} + +export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, modifiedTime?: Date) => void; +export type DirectoryWatcherCallback = (fileName: string) => void; +interface WatchedFile { + readonly fileName: string; + readonly callback: FileWatcherCallback; + mtime: Date; +} + +/** @internal */ +export enum PollingInterval { + High = 2000, + Medium = 500, + Low = 250, +} + +/** @internal */ +export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) => FileWatcher; +/** @internal */ +export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined) => FileWatcher; + +/** @internal */ +export const missingFileModifiedTime: Date = new Date(0); // Any subsequent modification will occur after this time + +/** @internal */ +export function getModifiedTime(host: { getModifiedTime: NonNullable; }, fileName: string): Date { + return host.getModifiedTime(fileName) || missingFileModifiedTime; +} + +interface Levels { + Low: number; + Medium: number; + High: number; +} + +function createPollingIntervalBasedLevels(levels: Levels) { + return { + [PollingInterval.Low]: levels.Low, + [PollingInterval.Medium]: levels.Medium, + [PollingInterval.High]: levels.High, + }; +} + +const defaultChunkLevels: Levels = { Low: 32, Medium: 64, High: 256 }; +let pollingChunkSize = createPollingIntervalBasedLevels(defaultChunkLevels); +/** @internal */ +export let unchangedPollThresholds: { [K in PollingInterval]: number; } = createPollingIntervalBasedLevels(defaultChunkLevels); + +function setCustomPollingValues(system: System) { + if (!system.getEnvironmentVariable) { + return; + } + const pollingIntervalChanged = setCustomLevels("TSC_WATCH_POLLINGINTERVAL", PollingInterval); + pollingChunkSize = getCustomPollingBasedLevels("TSC_WATCH_POLLINGCHUNKSIZE", defaultChunkLevels) || pollingChunkSize; + unchangedPollThresholds = getCustomPollingBasedLevels("TSC_WATCH_UNCHANGEDPOLLTHRESHOLDS", defaultChunkLevels) || unchangedPollThresholds; + + function getLevel(envVar: string, level: keyof Levels) { + return system.getEnvironmentVariable(`${envVar}_${level.toUpperCase()}`); + } + + function getCustomLevels(baseVariable: string) { + let customLevels: Partial | undefined; + setCustomLevel("Low"); + setCustomLevel("Medium"); + setCustomLevel("High"); + return customLevels; + + function setCustomLevel(level: keyof Levels) { + const customLevel = getLevel(baseVariable, level); + if (customLevel) { + (customLevels || (customLevels = {}))[level] = Number(customLevel); + } + } + } + + function setCustomLevels(baseVariable: string, levels: Levels) { + const customLevels = getCustomLevels(baseVariable); + if (customLevels) { + setLevel("Low"); + setLevel("Medium"); + setLevel("High"); + return true; + } + return false; + + function setLevel(level: keyof Levels) { + levels[level] = customLevels![level] || levels[level]; + } + } + + function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { + const customLevels = getCustomLevels(baseVariable); + return (pollingIntervalChanged || customLevels) && + createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); + } +} + +interface WatchedFileWithIsClosed extends WatchedFile { + isClosed?: boolean; +} +function pollWatchedFileQueue( + host: { getModifiedTime: NonNullable; }, + queue: (T | undefined)[], + pollIndex: number, + chunkSize: number, + callbackOnWatchFileStat?: (watchedFile: T, pollIndex: number, fileChanged: boolean) => void, +) { + let definedValueCopyToIndex = pollIndex; + // Max visit would be all elements of the queue + for (let canVisit = queue.length; chunkSize && canVisit; nextPollIndex(), canVisit--) { + const watchedFile = queue[pollIndex]; + if (!watchedFile) { + continue; + } + else if (watchedFile.isClosed) { + queue[pollIndex] = undefined; + continue; + } + + // Only files polled count towards chunkSize + chunkSize--; + const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(host, watchedFile.fileName)); + if (watchedFile.isClosed) { + // Closed watcher as part of callback + queue[pollIndex] = undefined; + continue; + } + + callbackOnWatchFileStat?.(watchedFile, pollIndex, fileChanged); + // Defragment the queue while we are at it + if (queue[pollIndex]) { + // Copy this file to the non hole location + if (definedValueCopyToIndex < pollIndex) { + queue[definedValueCopyToIndex] = watchedFile; + queue[pollIndex] = undefined; + } + definedValueCopyToIndex++; + } + } + + // Return next poll index + return pollIndex; + + function nextPollIndex() { + pollIndex++; + if (pollIndex === queue.length) { + if (definedValueCopyToIndex < pollIndex) { + // There are holes from definedValueCopyToIndex to end of queue, change queue size + queue.length = definedValueCopyToIndex; + } + pollIndex = 0; + definedValueCopyToIndex = 0; + } + } +} + +interface WatchedFileWithUnchangedPolls extends WatchedFileWithIsClosed { + unchangedPolls: number; +} +function createDynamicPriorityPollingWatchFile(host: { + getModifiedTime: NonNullable; + setTimeout: NonNullable; +}): HostWatchFile { + interface PollingIntervalQueue extends Array { + pollingInterval: PollingInterval; + pollIndex: number; + pollScheduled: boolean; + } + + const watchedFiles: WatchedFileWithUnchangedPolls[] = []; + const changedFilesInLastPoll: WatchedFileWithUnchangedPolls[] = []; + const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low); + const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium); + const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High); + return watchFile; + + function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher { + const file: WatchedFileWithUnchangedPolls = { + fileName, + callback, + unchangedPolls: 0, + mtime: getModifiedTime(host, fileName), + }; + watchedFiles.push(file); + + addToPollingIntervalQueue(file, defaultPollingInterval); + return { + close: () => { + file.isClosed = true; + // Remove from watchedFiles + unorderedRemoveItem(watchedFiles, file); + // Do not update polling interval queue since that will happen as part of polling + }, + }; + } + + function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { + const queue = [] as WatchedFileWithUnchangedPolls[] as PollingIntervalQueue; + queue.pollingInterval = pollingInterval; + queue.pollIndex = 0; + queue.pollScheduled = false; + return queue; + } + + function pollPollingIntervalQueue(_timeoutType: string, queue: PollingIntervalQueue) { + queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, pollingChunkSize[queue.pollingInterval]); + // Set the next polling index and timeout + if (queue.length) { + scheduleNextPoll(queue.pollingInterval); + } + else { + Debug.assert(queue.pollIndex === 0); + queue.pollScheduled = false; + } + } + + function pollLowPollingIntervalQueue(_timeoutType: string, queue: PollingIntervalQueue) { + // Always poll complete list of changedFilesInLastPoll + pollQueue(changedFilesInLastPoll, PollingInterval.Low, /*pollIndex*/ 0, changedFilesInLastPoll.length); + + // Finally do the actual polling of the queue + pollPollingIntervalQueue(_timeoutType, queue); + // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue + // as pollPollingIntervalQueue wont schedule for next poll + if (!queue.pollScheduled && changedFilesInLastPoll.length) { + scheduleNextPoll(PollingInterval.Low); + } + } + + function pollQueue(queue: (WatchedFileWithUnchangedPolls | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) { + return pollWatchedFileQueue( + host, + queue, + pollIndex, + chunkSize, + onWatchFileStat, + ); + + function onWatchFileStat(watchedFile: WatchedFileWithUnchangedPolls, pollIndex: number, fileChanged: boolean) { + if (fileChanged) { + watchedFile.unchangedPolls = 0; + // Changed files go to changedFilesInLastPoll queue + if (queue !== changedFilesInLastPoll) { + queue[pollIndex] = undefined; + addChangedFileToLowPollingIntervalQueue(watchedFile); + } + } + else if (watchedFile.unchangedPolls !== unchangedPollThresholds[pollingInterval]) { + watchedFile.unchangedPolls++; + } + else if (queue === changedFilesInLastPoll) { + // Restart unchangedPollCount for unchanged file and move to low polling interval queue + watchedFile.unchangedPolls = 1; + queue[pollIndex] = undefined; + addToPollingIntervalQueue(watchedFile, PollingInterval.Low); + } + else if (pollingInterval !== PollingInterval.High) { + watchedFile.unchangedPolls++; + queue[pollIndex] = undefined; + addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High); + } + } + } + + function pollingIntervalQueue(pollingInterval: PollingInterval) { + switch (pollingInterval) { + case PollingInterval.Low: + return lowPollingIntervalQueue; + case PollingInterval.Medium: + return mediumPollingIntervalQueue; + case PollingInterval.High: + return highPollingIntervalQueue; + } + } + + function addToPollingIntervalQueue(file: WatchedFileWithUnchangedPolls, pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).push(file); + scheduleNextPollIfNotAlreadyScheduled(pollingInterval); + } + + function addChangedFileToLowPollingIntervalQueue(file: WatchedFileWithUnchangedPolls) { + changedFilesInLastPoll.push(file); + scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); + } + + function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { + if (!pollingIntervalQueue(pollingInterval).pollScheduled) { + scheduleNextPoll(pollingInterval); + } + } + + function scheduleNextPoll(pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingInterval === PollingInterval.Low ? "pollLowPollingIntervalQueue" : "pollPollingIntervalQueue", pollingIntervalQueue(pollingInterval)); + } +} + +function createUseFsEventsOnParentDirectoryWatchFile( + fsWatch: FsWatch, + useCaseSensitiveFileNames: boolean, + getModifiedTime: NonNullable, + fsWatchWithTimestamp: boolean | undefined, +): HostWatchFile { + // One file can have multiple watchers + const fileWatcherCallbacks = createMultiMap(); + const fileTimestamps = fsWatchWithTimestamp ? new Map() : undefined; + const dirWatchers = new Map(); + const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return nonPollingWatchFile; + + function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { + const filePath = toCanonicalName(fileName); + if (fileWatcherCallbacks.add(filePath, callback).length === 1 && fileTimestamps) { + fileTimestamps.set(filePath, getModifiedTime(fileName) || missingFileModifiedTime); + } + const dirPath = getDirectoryPath(filePath) || "."; + const watcher = dirWatchers.get(dirPath) || + createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); + 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, fallbackOptions: WatchOptions | undefined) { + const watcher = fsWatch( + dirName, + FileSystemEntryKind.Directory, + (eventName: string, relativeFileName) => { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + if (!isString(relativeFileName)) return; + const fileName = getNormalizedAbsolutePath(relativeFileName, dirName); + const filePath = toCanonicalName(fileName); + // Some applications save a working file via rename operations + const callbacks = fileName && fileWatcherCallbacks.get(filePath); + if (callbacks) { + let currentModifiedTime; + let eventKind = FileWatcherEventKind.Changed; + if (fileTimestamps) { + const existingTime = fileTimestamps.get(filePath)!; + if (eventName === "change") { + currentModifiedTime = getModifiedTime(fileName) || missingFileModifiedTime; + if (currentModifiedTime.getTime() === existingTime.getTime()) return; + } + currentModifiedTime ||= getModifiedTime(fileName) || missingFileModifiedTime; + fileTimestamps.set(filePath, currentModifiedTime); + if (existingTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Created; + else if (currentModifiedTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Deleted; + } + for (const fileCallback of callbacks) { + fileCallback(fileName, eventKind, currentModifiedTime); + } + } + }, + /*recursive*/ false, + PollingInterval.Medium, + fallbackOptions, + ) as DirectoryWatcher; + watcher.referenceCount = 0; + dirWatchers.set(dirPath, watcher); + return watcher; + } +} + +function createFixedChunkSizePollingWatchFile(host: { + getModifiedTime: NonNullable; + setTimeout: NonNullable; +}): HostWatchFile { + const watchedFiles: (WatchedFileWithIsClosed | undefined)[] = []; + let pollIndex = 0; + let pollScheduled: any; + return watchFile; + + function watchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { + const file: WatchedFileWithIsClosed = { + fileName, + callback, + mtime: getModifiedTime(host, fileName), + }; + watchedFiles.push(file); + scheduleNextPoll(); + return { + close: () => { + file.isClosed = true; + unorderedRemoveItem(watchedFiles, file); + }, + }; + } + + function pollQueue() { + pollScheduled = undefined; + pollIndex = pollWatchedFileQueue(host, watchedFiles, pollIndex, pollingChunkSize[PollingInterval.Low]); + scheduleNextPoll(); + } + + function scheduleNextPoll() { + if (!watchedFiles.length || pollScheduled) return; + pollScheduled = host.setTimeout(pollQueue, PollingInterval.High, "pollQueue"); + } +} + +interface SingleFileWatcher { + watcher: FileWatcher; + callbacks: T[]; +} +function createSingleWatcherPerName( + cache: Map>, + useCaseSensitiveFileNames: boolean, + name: string, + callback: T, + createWatcher: (callback: T) => FileWatcher, +): FileWatcher { + const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + const path = toCanonicalFileName(name); + const existing = cache.get(path); + if (existing) { + existing.callbacks.push(callback); + } + else { + cache.set(path, { + watcher: createWatcher( + ( + // Cant infer types correctly so lets satisfy checker + (param1: any, param2: never, param3: any) => cache.get(path)?.callbacks.slice().forEach(cb => cb(param1, param2, param3)) + ) as T, + ), + callbacks: [callback], + }); + } + + return { + close: () => { + const watcher = cache.get(path); + // Watcher is not expected to be undefined, but if it is normally its because + // exception was thrown somewhere else and watch state is not what it should be + if (!watcher) return; + if (!orderedRemoveItem(watcher.callbacks, callback) || watcher.callbacks.length) return; + cache.delete(path); + closeFileWatcherOf(watcher); + }, + }; +} + +/** + * Returns true if file status changed + */ +function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean { + const oldTime = watchedFile.mtime.getTime(); + const newTime = modifiedTime.getTime(); + if (oldTime !== newTime) { + watchedFile.mtime = modifiedTime; + // Pass modified times so tsc --build can use it + watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime), modifiedTime); + return true; + } + + return false; +} + +/** @internal */ +export function getFileWatcherEventKind(oldTime: number, newTime: number): FileWatcherEventKind { + return oldTime === 0 + ? FileWatcherEventKind.Created + : newTime === 0 + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; +} + +/** @internal */ +export const ignoredPaths: readonly string[] = ["/node_modules/.", "/.git", "/.#"]; + +let curSysLog: (s: string) => void = noop; + +/** @internal */ +export function sysLog(s: string): void { + return curSysLog(s); +} + +/** @internal */ +export function setSysLog(logger: typeof sysLog): void { + curSysLog = logger; +} + +interface RecursiveDirectoryWatcherHost { + watchDirectory: HostWatchDirectory; + useCaseSensitiveFileNames: boolean; + getCurrentDirectory: System["getCurrentDirectory"]; + getAccessibleSortedChildDirectories(path: string): readonly string[]; + fileSystemEntryExists: FileSystemEntryExists; + realpath(s: string): string; + setTimeout: NonNullable; + clearTimeout: NonNullable; +} + +/** + * Watch the directory recursively using host provided method to watch child directories + * 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) + */ +function createDirectoryWatcherSupportingRecursive({ + watchDirectory, + useCaseSensitiveFileNames, + getCurrentDirectory, + getAccessibleSortedChildDirectories, + fileSystemEntryExists, + realpath, + setTimeout, + clearTimeout, +}: RecursiveDirectoryWatcherHost): HostWatchDirectory { + interface ChildDirectoryWatcher extends FileWatcher { + dirName: string; + } + type ChildWatches = readonly ChildDirectoryWatcher[]; + interface HostDirectoryWatcher { + watcher: FileWatcher; + childWatches: ChildWatches; + refCount: number; + targetWatcher: ChildDirectoryWatcher | undefined; + links: Set | undefined; + } + + const cache = new Map(); + const callbackCache = createMultiMap(); + const cacheToUpdateChildWatches = new Map(); + let timerToUpdateChildWatches: any; + + const filePathComparer = getStringComparer(!useCaseSensitiveFileNames); + const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames) as (fileName: string) => Path; + + return (dirName, callback, recursive, options) => + recursive ? + createDirectoryWatcher(dirName, options, callback) : + watchDirectory(dirName, callback, recursive, options); + + /** + * Create the directory watcher for the dirPath. + */ + function createDirectoryWatcher( + dirName: string, + options: WatchOptions | undefined, + callback?: DirectoryWatcherCallback, + link?: string, + ): ChildDirectoryWatcher { + const dirPath = toCanonicalFilePath(dirName); + let directoryWatcher = cache.get(dirPath); + if (directoryWatcher) { + directoryWatcher.refCount++; + } + else { + directoryWatcher = { + watcher: watchDirectory( + dirName, + fileName => { + if (isIgnoredPath(fileName, options)) return; + + if (options?.synchronousWatchDirectory) { + // Call the actual callback + if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, fileName); + + // Iterate through existing children and update the watches if needed + updateChildWatches(dirName, dirPath, options); + } + else { + nonSyncUpdateChildWatches(dirName, dirPath, fileName, options); + } + }, + /*recursive*/ false, + options, + ), + refCount: 1, + childWatches: emptyArray, + targetWatcher: undefined, + links: undefined, + }; + cache.set(dirPath, directoryWatcher); + updateChildWatches(dirName, dirPath, options); + } + + if (link) (directoryWatcher.links ??= new Set()).add(link); + + const callbackToAdd = callback && { dirName, callback }; + if (callbackToAdd) { + callbackCache.add(dirPath, callbackToAdd); + } + + return { + dirName, + close: () => { + const directoryWatcher = Debug.checkDefined(cache.get(dirPath)); + if (callbackToAdd) callbackCache.remove(dirPath, callbackToAdd); + if (link) directoryWatcher.links?.delete(link); + directoryWatcher.refCount--; + + if (directoryWatcher.refCount) return; + + cache.delete(dirPath); + directoryWatcher.links = undefined; + closeFileWatcherOf(directoryWatcher); + closeTargetWatcher(directoryWatcher); + directoryWatcher.childWatches.forEach(closeFileWatcher); + }, + }; + } + + type InvokeMap = Map; + function invokeCallbacks(dirName: string, dirPath: Path, fileName: string): void; + function invokeCallbacks(dirName: string, dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void; + function invokeCallbacks(dirName: string, dirPath: Path, fileNameOrInvokeMap: string | InvokeMap, fileNames?: string[]) { + let fileName: string | undefined; + let invokeMap: InvokeMap | undefined; + if (isString(fileNameOrInvokeMap)) { + fileName = fileNameOrInvokeMap; + } + else { + invokeMap = fileNameOrInvokeMap; + } + // Call the actual callback + callbackCache.forEach((callbacks, rootDirName) => { + if (invokeMap && invokeMap.get(rootDirName) === true) return; + if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) { + if (invokeMap) { + if (fileNames) { + const existing = invokeMap.get(rootDirName); + if (existing) { + (existing as string[]).push(...fileNames); + } + else { + invokeMap.set(rootDirName, fileNames.slice()); + } + } + else { + invokeMap.set(rootDirName, true); + } + } + else { + callbacks.forEach(({ callback }) => callback(fileName!)); + } + } + }); + cache.get(dirPath)?.links?.forEach(link => { + const toPathInLink = (fileName: string) => combinePaths(link, getRelativePathFromDirectory(dirName, fileName, toCanonicalFilePath)); + if (invokeMap) { + invokeCallbacks(link, toCanonicalFilePath(link), invokeMap, fileNames?.map(toPathInLink)); + } + else { + invokeCallbacks(link, toCanonicalFilePath(link), toPathInLink(fileName!)); + } + }); + } + + function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) { + // Iterate through existing children and update the watches if needed + const parentWatcher = cache.get(dirPath); + if (parentWatcher && fileSystemEntryExists(dirName, FileSystemEntryKind.Directory)) { + // Schedule the update and postpone invoke for callbacks + scheduleUpdateChildWatches(dirName, dirPath, fileName, options); + return; + } + + // Call the actual callbacks and remove child watches + invokeCallbacks(dirName, dirPath, fileName); + closeTargetWatcher(parentWatcher); + removeChildWatches(parentWatcher); + } + + function scheduleUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) { + const existing = cacheToUpdateChildWatches.get(dirPath); + if (existing) { + existing.fileNames.push(fileName); + } + else { + cacheToUpdateChildWatches.set(dirPath, { dirName, options, fileNames: [fileName] }); + } + if (timerToUpdateChildWatches) { + clearTimeout(timerToUpdateChildWatches); + timerToUpdateChildWatches = undefined; + } + timerToUpdateChildWatches = setTimeout(onTimerToUpdateChildWatches, 1000, "timerToUpdateChildWatches"); + } + + function onTimerToUpdateChildWatches() { + timerToUpdateChildWatches = undefined; + sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`); + const start = timestamp(); + const invokeMap = new Map(); + + while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) { + const result = cacheToUpdateChildWatches.entries().next(); + Debug.assert(!result.done); + const { value: [dirPath, { dirName, options, fileNames }] } = result; + cacheToUpdateChildWatches.delete(dirPath); + // Because the child refresh is fresh, we would need to invalidate whole root directory being watched + // to ensure that all the changes are reflected at this time + const hasChanges = updateChildWatches(dirName, dirPath, options); + if (!cache.get(dirPath)?.targetWatcher) invokeCallbacks(dirName, dirPath, invokeMap, hasChanges ? undefined : fileNames); + } + + sysLog(`sysLog:: invokingWatchers:: Elapsed:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`); + callbackCache.forEach((callbacks, rootDirName) => { + const existing = invokeMap.get(rootDirName); + if (existing) { + callbacks.forEach(({ callback, dirName }) => { + if (isArray(existing)) { + existing.forEach(callback); + } + else { + callback(dirName); + } + }); + } + }); + + const elapsed = timestamp() - start; + sysLog(`sysLog:: Elapsed:: ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`); + } + + function removeChildWatches(parentWatcher: HostDirectoryWatcher | undefined) { + if (!parentWatcher) return; + const existingChildWatches = parentWatcher.childWatches; + parentWatcher.childWatches = emptyArray; + for (const childWatcher of existingChildWatches) { + childWatcher.close(); + removeChildWatches(cache.get(toCanonicalFilePath(childWatcher.dirName))); + } + } + + function closeTargetWatcher(watcher: HostDirectoryWatcher | undefined) { + if (watcher?.targetWatcher) { + watcher.targetWatcher.close(); + watcher.targetWatcher = undefined; + } + } + + function updateChildWatches(parentDir: string, parentDirPath: Path, options: WatchOptions | undefined) { + // Iterate through existing children and update the watches if needed + const parentWatcher = cache.get(parentDirPath); + if (!parentWatcher) return false; + const target = normalizePath(realpath(parentDir)); + let hasChanges; + let newChildWatches: ChildDirectoryWatcher[] | undefined; + if (filePathComparer(target, parentDir) === Comparison.EqualTo) { + // if (parentWatcher.target) closeFileWatcher + hasChanges = enumerateInsertsAndDeletes( + fileSystemEntryExists(parentDir, FileSystemEntryKind.Directory) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => { + const childFullName = getNormalizedAbsolutePath(child, parentDir); + // Filter our the symbolic link directories since those arent included in recursive watch + // which is same behaviour when recursive: true is passed to fs.watch + return !isIgnoredPath(childFullName, options) && filePathComparer(childFullName, normalizePath(realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined; + }) : emptyArray, + parentWatcher.childWatches, + (child, childWatcher) => filePathComparer(child, childWatcher.dirName), + createAndAddChildDirectoryWatcher, + closeFileWatcher, + addChildDirectoryWatcher, + ); + } + else if (parentWatcher.targetWatcher && filePathComparer(target, parentWatcher.targetWatcher.dirName) === Comparison.EqualTo) { + hasChanges = false; + Debug.assert(parentWatcher.childWatches === emptyArray); + } + else { + closeTargetWatcher(parentWatcher); + parentWatcher.targetWatcher = createDirectoryWatcher(target, options, /*callback*/ undefined, parentDir); + parentWatcher.childWatches.forEach(closeFileWatcher); + hasChanges = true; + } + parentWatcher.childWatches = newChildWatches || emptyArray; + return hasChanges; + + /** + * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list + */ + function createAndAddChildDirectoryWatcher(childName: string) { + const result = createDirectoryWatcher(childName, options); + addChildDirectoryWatcher(result); + } + + /** + * Add child directory watcher to the new ChildDirectoryWatcher list + */ + function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) { + (newChildWatches || (newChildWatches = [])).push(childWatcher); + } + } + + function isIgnoredPath(path: string, options: WatchOptions | undefined) { + return some(ignoredPaths, searchPath => isInPath(path, searchPath)) || + isIgnoredByWatchOptions(path, options, useCaseSensitiveFileNames, getCurrentDirectory); + } + + function isInPath(path: string, searchPath: string) { + if (path.includes(searchPath)) return true; + if (useCaseSensitiveFileNames) return false; + return toCanonicalFilePath(path).includes(searchPath); + } +} + +/** @internal */ +export type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined | null, modifiedTime?: Date) => void; // eslint-disable-line no-restricted-syntax +/** @internal */ +export type FsWatch = (fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined) => FileWatcher; +/** @internal */ +export interface FsWatchWorkerWatcher extends FileWatcher { + on(eventName: string, listener: () => void): void; +} +/** @internal */ +export type FsWatchWorker = (fileOrDirectory: string, recursive: boolean, callback: FsWatchCallback) => FsWatchWorkerWatcher; +/** @internal */ +export const enum FileSystemEntryKind { + File, + Directory, +} + +function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { + return (_fileName, eventKind, modifiedTime) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "", modifiedTime); +} + +function createFsWatchCallbackForFileWatcherCallback( + fileName: string, + callback: FileWatcherCallback, + getModifiedTime: NonNullable, +): FsWatchCallback { + return (eventName, _relativeFileName, modifiedTime) => { + if (eventName === "rename") { + // Check time stamps rather than file system entry checks + modifiedTime ||= getModifiedTime(fileName) || missingFileModifiedTime; + callback(fileName, modifiedTime !== missingFileModifiedTime ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted, modifiedTime); + } + else { + // Change + callback(fileName, FileWatcherEventKind.Changed, modifiedTime); + } + }; +} + +function isIgnoredByWatchOptions( + pathToCheck: string, + options: WatchOptions | undefined, + useCaseSensitiveFileNames: boolean, + getCurrentDirectory: System["getCurrentDirectory"], +) { + return (options?.excludeDirectories || options?.excludeFiles) && ( + matchesExclude(pathToCheck, options?.excludeFiles, useCaseSensitiveFileNames, getCurrentDirectory()) || + matchesExclude(pathToCheck, options?.excludeDirectories, useCaseSensitiveFileNames, getCurrentDirectory()) + ); +} + +function createFsWatchCallbackForDirectoryWatcherCallback( + directoryName: string, + callback: DirectoryWatcherCallback, + options: WatchOptions | undefined, + useCaseSensitiveFileNames: boolean, + getCurrentDirectory: System["getCurrentDirectory"], +): FsWatchCallback { + return (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 + const fileName = !relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName)); + if (!relativeFileName || !isIgnoredByWatchOptions(fileName, options, useCaseSensitiveFileNames, getCurrentDirectory)) { + callback(fileName); + } + } + }; +} + +/** @internal */ +export type FileSystemEntryExists = (fileorDirectrory: string, entryKind: FileSystemEntryKind) => boolean; + +/** @internal */ +export interface CreateSystemWatchFunctions { + // Polling watch file + pollingWatchFileWorker: HostWatchFile; + // For dynamic polling watch file + getModifiedTime: NonNullable; + setTimeout: NonNullable; + clearTimeout: NonNullable; + // For fs events : + fsWatchWorker: FsWatchWorker; + fileSystemEntryExists: FileSystemEntryExists; + useCaseSensitiveFileNames: boolean; + getCurrentDirectory: System["getCurrentDirectory"]; + fsSupportsRecursiveFsWatch: boolean; + getAccessibleSortedChildDirectories(path: string): readonly string[]; + realpath(s: string): string; + // For backward compatibility environment variables + tscWatchFile: string | undefined; + useNonPollingWatchers?: boolean; + tscWatchDirectory: string | undefined; + inodeWatching: boolean; + fsWatchWithTimestamp: boolean | undefined; + sysLog: (s: string) => void; +} + +/** @internal */ +export function createSystemWatchFunctions({ + pollingWatchFileWorker, + getModifiedTime, + setTimeout, + clearTimeout, + fsWatchWorker, + fileSystemEntryExists, + useCaseSensitiveFileNames, + getCurrentDirectory, + fsSupportsRecursiveFsWatch, + getAccessibleSortedChildDirectories, + realpath, + tscWatchFile, + useNonPollingWatchers, + tscWatchDirectory, + inodeWatching, + fsWatchWithTimestamp, + sysLog, +}: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } { + const pollingWatches = new Map>(); + const fsWatches = new Map>(); + const fsWatchesRecursive = new Map>(); + let dynamicPollingWatchFile: HostWatchFile | undefined; + let fixedChunkSizePollingWatchFile: HostWatchFile | undefined; + let nonPollingWatchFile: HostWatchFile | undefined; + let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined; + let hitSystemWatcherLimit = false; + return { + watchFile, + watchDirectory, + }; + + function watchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined): FileWatcher { + options = updateOptionsForWatchFile(options, useNonPollingWatchers); + const watchFileKind = Debug.checkDefined(options.watchFile); + switch (watchFileKind) { + case WatchFileKind.FixedPollingInterval: + return pollingWatchFile(fileName, callback, PollingInterval.Low, /*options*/ undefined); + case WatchFileKind.PriorityPollingInterval: + return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined); + case WatchFileKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined); + case WatchFileKind.FixedChunkSizePolling: + return ensureFixedChunkSizePollingWatchFile()(fileName, callback, /* pollingInterval */ undefined!, /*options*/ undefined); + case WatchFileKind.UseFsEvents: + return fsWatch( + fileName, + FileSystemEntryKind.File, + createFsWatchCallbackForFileWatcherCallback(fileName, callback, getModifiedTime), + /*recursive*/ false, + pollingInterval, + getFallbackOptions(options), + ); + case WatchFileKind.UseFsEventsOnParentDirectory: + if (!nonPollingWatchFile) { + nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames, getModifiedTime, fsWatchWithTimestamp); + } + return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options)); + default: + Debug.assertNever(watchFileKind); + } + } + + function ensureDynamicPollingWatchFile() { + return dynamicPollingWatchFile ||= createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); + } + + function ensureFixedChunkSizePollingWatchFile() { + return fixedChunkSizePollingWatchFile ||= createFixedChunkSizePollingWatchFile({ getModifiedTime, setTimeout }); + } + + function updateOptionsForWatchFile(options: WatchOptions | undefined, useNonPollingWatchers?: boolean): WatchOptions { + if (options && options.watchFile !== undefined) return options; + switch (tscWatchFile) { + case "PriorityPollingInterval": + // Use polling interval based on priority when create watch using host.watchFile + return { watchFile: WatchFileKind.PriorityPollingInterval }; + case "DynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchFile: WatchFileKind.DynamicPriorityPolling }; + case "UseFsEvents": + // Use notifications from FS to watch with falling back to fs.watchFile + return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.PriorityInterval, options); + case "UseFsEventsWithFallbackDynamicPolling": + // Use notifications from FS to watch with falling back to dynamic watch file + return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.DynamicPriority, options); + case "UseFsEventsOnParentDirectory": + useNonPollingWatchers = true; + // fall through + default: + return useNonPollingWatchers ? + // Use notifications from FS to watch with falling back to fs.watchFile + generateWatchFileOptions(WatchFileKind.UseFsEventsOnParentDirectory, PollingWatchKind.PriorityInterval, options) : + // Default to using fs events + { watchFile: WatchFileKind.UseFsEvents }; + } + } + + function generateWatchFileOptions( + watchFile: WatchFileKind, + fallbackPolling: PollingWatchKind, + options: WatchOptions | undefined, + ): WatchOptions { + const defaultFallbackPolling = options?.fallbackPolling; + return { + watchFile, + fallbackPolling: defaultFallbackPolling === undefined ? + fallbackPolling : + defaultFallbackPolling, + }; + } + + function watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { + if (fsSupportsRecursiveFsWatch) { + return fsWatch( + directoryName, + FileSystemEntryKind.Directory, + createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), + recursive, + PollingInterval.Medium, + getFallbackOptions(options), + ); + } + + if (!hostRecursiveDirectoryWatcher) { + hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({ + useCaseSensitiveFileNames, + getCurrentDirectory, + fileSystemEntryExists, + getAccessibleSortedChildDirectories, + watchDirectory: nonRecursiveWatchDirectory, + realpath, + setTimeout, + clearTimeout, + }); + } + return hostRecursiveDirectoryWatcher(directoryName, callback, recursive, options); + } + + function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { + Debug.assert(!recursive); + const watchDirectoryOptions = updateOptionsForWatchDirectory(options); + const watchDirectoryKind = Debug.checkDefined(watchDirectoryOptions.watchDirectory); + switch (watchDirectoryKind) { + case WatchDirectoryKind.FixedPollingInterval: + return pollingWatchFile( + directoryName, + () => callback(directoryName), + PollingInterval.Medium, + /*options*/ undefined, + ); + case WatchDirectoryKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()( + directoryName, + () => callback(directoryName), + PollingInterval.Medium, + /*options*/ undefined, + ); + case WatchDirectoryKind.FixedChunkSizePolling: + return ensureFixedChunkSizePollingWatchFile()( + directoryName, + () => callback(directoryName), + /* pollingInterval */ undefined!, + /*options*/ undefined, + ); + case WatchDirectoryKind.UseFsEvents: + return fsWatch( + directoryName, + FileSystemEntryKind.Directory, + createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory), + recursive, + PollingInterval.Medium, + getFallbackOptions(watchDirectoryOptions), + ); + default: + Debug.assertNever(watchDirectoryKind); + } + } + + function updateOptionsForWatchDirectory(options: WatchOptions | undefined): WatchOptions { + if (options && options.watchDirectory !== undefined) return options; + switch (tscWatchDirectory) { + case "RecursiveDirectoryUsingFsWatchFile": + // Use polling interval based on priority when create watch using host.watchFile + return { watchDirectory: WatchDirectoryKind.FixedPollingInterval }; + case "RecursiveDirectoryUsingDynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchDirectory: WatchDirectoryKind.DynamicPriorityPolling }; + default: + const defaultFallbackPolling = options?.fallbackPolling; + return { + watchDirectory: WatchDirectoryKind.UseFsEvents, + fallbackPolling: defaultFallbackPolling !== undefined ? + defaultFallbackPolling : + undefined, + }; + } + } + + function pollingWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) { + return createSingleWatcherPerName( + pollingWatches, + useCaseSensitiveFileNames, + fileName, + callback, + cb => pollingWatchFileWorker(fileName, cb, pollingInterval, options), + ); + } + function fsWatch( + fileOrDirectory: string, + entryKind: FileSystemEntryKind, + callback: FsWatchCallback, + recursive: boolean, + fallbackPollingInterval: PollingInterval, + fallbackOptions: WatchOptions | undefined, + ): FileWatcher { + return createSingleWatcherPerName( + recursive ? fsWatchesRecursive : fsWatches, + useCaseSensitiveFileNames, + fileOrDirectory, + callback, + cb => fsWatchHandlingExistenceOnHost(fileOrDirectory, entryKind, cb, recursive, fallbackPollingInterval, fallbackOptions), + ); + } + + function fsWatchHandlingExistenceOnHost( + fileOrDirectory: string, + entryKind: FileSystemEntryKind, + callback: FsWatchCallback, + recursive: boolean, + fallbackPollingInterval: PollingInterval, + fallbackOptions: WatchOptions | undefined, + ): FileWatcher { + let lastDirectoryPartWithDirectorySeparator: string | undefined; + let lastDirectoryPart: string | undefined; + if (inodeWatching) { + lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substring(fileOrDirectory.lastIndexOf(directorySeparator)); + lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(directorySeparator.length); + } + /** Watcher for the file system entry depending on whether it is missing or present */ + let watcher: FileWatcher | undefined = !fileSystemEntryExists(fileOrDirectory, entryKind) ? + watchMissingFileSystemEntry() : + watchPresentFileSystemEntry(); + return { + close: () => { + // Close the watcher (either existing file system entry watcher or missing file system entry watcher) + if (watcher) { + watcher.close(); + watcher = undefined; + } + }, + }; + + function updateWatcher(createWatcher: () => FileWatcher) { + // If watcher is not closed, update it + if (watcher) { + sysLog(`sysLog:: ${fileOrDirectory}:: Changing watcher to ${createWatcher === watchPresentFileSystemEntry ? "Present" : "Missing"}FileSystemEntryWatcher`); + watcher.close(); + watcher = createWatcher(); + } + } + + /** + * Watch the file or directory that is currently present + * and when the watched file or directory is deleted, switch to missing file system entry watcher + */ + function watchPresentFileSystemEntry(): FileWatcher { + if (hitSystemWatcherLimit) { + sysLog(`sysLog:: ${fileOrDirectory}:: Defaulting to watchFile`); + return watchPresentFileSystemEntryWithFsWatchFile(); + } + try { + const presentWatcher = (entryKind === FileSystemEntryKind.Directory || !fsWatchWithTimestamp ? fsWatchWorker : fsWatchWorkerHandlingTimestamp)( + fileOrDirectory, + recursive, + inodeWatching ? + callbackChangingToMissingFileSystemEntry : + callback, + ); + // Watch the missing file or directory or error + presentWatcher.on("error", () => { + callback("rename", ""); + updateWatcher(watchMissingFileSystemEntry); + }); + return presentWatcher; + } + 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 fs.watchFile + hitSystemWatcherLimit ||= e.code === "ENOSPC"; + sysLog(`sysLog:: ${fileOrDirectory}:: Changing to watchFile`); + return watchPresentFileSystemEntryWithFsWatchFile(); + } + } + + function callbackChangingToMissingFileSystemEntry(event: "rename" | "change", relativeName: string | undefined | null) { // eslint-disable-line no-restricted-syntax + // In some scenarios, file save operation fires event with fileName.ext~ instead of fileName.ext + // To ensure we see the file going missing and coming back up (file delete and then recreated) + // and watches being updated correctly we are calling back with fileName.ext as well as fileName.ext~ + // The worst is we have fired event that was not needed but we wont miss any changes + // especially in cases where file goes missing and watches wrong inode + let originalRelativeName: string | undefined; + if (relativeName && endsWith(relativeName, "~")) { + originalRelativeName = relativeName; + relativeName = relativeName.slice(0, relativeName.length - 1); + } + // because relativeName is not guaranteed to be correct we need to check on each rename with few combinations + // Eg on ubuntu while watching app/node_modules the relativeName is "node_modules" which is neither relative nor full path + if ( + event === "rename" && + (!relativeName || + relativeName === lastDirectoryPart || + endsWith(relativeName, lastDirectoryPartWithDirectorySeparator!)) + ) { + const modifiedTime = getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + if (originalRelativeName) callback(event, originalRelativeName, modifiedTime); + callback(event, relativeName, modifiedTime); + if (inodeWatching) { + // If this was rename event, inode has changed means we need to update watcher + updateWatcher(modifiedTime === missingFileModifiedTime ? watchMissingFileSystemEntry : watchPresentFileSystemEntry); + } + else if (modifiedTime === missingFileModifiedTime) { + updateWatcher(watchMissingFileSystemEntry); + } + } + else { + if (originalRelativeName) callback(event, originalRelativeName); + callback(event, relativeName); + } + } + + /** + * Watch the file or directory using fs.watchFile since fs.watch threw exception + * 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 + */ + function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { + return watchFile( + fileOrDirectory, + createFileWatcherCallback(callback), + fallbackPollingInterval, + fallbackOptions, + ); + } + + /** + * Watch the file or directory that is missing + * and switch to existing file or directory when the missing filesystem entry is created + */ + function watchMissingFileSystemEntry(): FileWatcher { + return watchFile( + fileOrDirectory, + (_fileName, eventKind, modifiedTime) => { + if (eventKind === FileWatcherEventKind.Created) { + modifiedTime ||= getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + if (modifiedTime !== missingFileModifiedTime) { + callback("rename", "", modifiedTime); + // Call the callback for current file or directory + // For now it could be callback for the inner directory creation, + // but just return current directory, better than current no-op + updateWatcher(watchPresentFileSystemEntry); + } + } + }, + fallbackPollingInterval, + fallbackOptions, + ); + } + } + + function fsWatchWorkerHandlingTimestamp(fileOrDirectory: string, recursive: boolean, callback: FsWatchCallback): FsWatchWorkerWatcher { + let modifiedTime = getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + return fsWatchWorker(fileOrDirectory, recursive, (eventName, relativeFileName, currentModifiedTime) => { + if (eventName === "change") { + currentModifiedTime ||= getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + if (currentModifiedTime.getTime() === modifiedTime.getTime()) return; + } + modifiedTime = currentModifiedTime || getModifiedTime(fileOrDirectory) || missingFileModifiedTime; + callback(eventName, relativeFileName, modifiedTime); + }); + } +} + +/** + * patch writefile to create folder before writing the file + * + * @internal + */ +export function patchWriteFileEnsuringDirectory(sys: System): void { + // patch writefile to create folder before writing the file + const originalWriteFile = sys.writeFile; + sys.writeFile = (path, data, writeBom) => + writeFileEnsuringDirectories( + path, + data, + !!writeBom, + (path, data, writeByteOrderMark) => originalWriteFile.call(sys, path, data, writeByteOrderMark), + path => sys.createDirectory(path), + path => sys.directoryExists(path), + ); +} + +export type BufferEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"; + +// TODO: GH#18217 Methods on System are often used as if they are certainly defined +export interface System { + args: string[]; + newLine: string; + useCaseSensitiveFileNames: boolean; + write(s: string): void; + writeOutputIsTTY?(): boolean; + getWidthOfTerminal?(): number; + readFile(path: string, encoding?: string): string | undefined; + getFileSize?(path: string): number; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + + /** + * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that + * use native OS file watching + */ + watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher; + watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher; + /**@internal */ preferNonRecursiveWatch?: boolean; + resolvePath(path: string): string; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + getExecutingFilePath(): string; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[]; + getModifiedTime?(path: string): Date | undefined; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; + /** + * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) + */ + createHash?(data: string): string; + /** This must be cryptographically secure. Only implement this method using `crypto.createHash("sha256")`. */ + createSHA256Hash?(data: string): string; + getMemoryUsage?(): number; + exit(exitCode?: number): void; + /** @internal */ enableCPUProfiler?(path: string, continuation: () => void): boolean; + /** @internal */ disableCPUProfiler?(continuation: () => void): boolean; + /** @internal */ cpuProfilingEnabled?(): boolean; + realpath?(path: string): string; + /** @internal */ getEnvironmentVariable(name: string): string; + /** @internal */ tryEnableSourceMapsForHost?(): void; + /** @internal */ getAccessibleFileSystemEntries?(path: string): FileSystemEntries; + /** @internal */ debugMode?: boolean; + setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; + clearTimeout?(timeoutId: any): void; + clearScreen?(): void; + /** @internal */ setBlocking?(): void; + base64decode?(input: string): string; + base64encode?(input: string): string; + /** @internal */ require?(baseDir: string, moduleName: string): ModuleImportResult; + + // For testing + /** @internal */ now?(): Date; + /** @internal */ storeSignatureInfo?: boolean; +} + +export interface FileWatcher { + close(): void; +} + +interface DirectoryWatcher extends FileWatcher { + referenceCount: number; +} + +// TODO: GH#18217 this is used as if it's certainly defined in many places. +export let sys: System = (() => { + // NodeJS detects "\uFEFF" at the start of the string and *replaces* it with the actual + // byte order mark from the specified encoding. Using any other byte order mark does + // not actually work. + const byteOrderMarkIndicator = "\uFEFF"; + + function getNodeSystem(): System { + const nativePattern = /^native |^\([^)]+\)$|^(?:internal[\\/]|[\w\s]+(?:\.js)?$)/; + const _fs: typeof import("fs") = require("fs"); + const _path: typeof import("path") = require("path"); + const _os = require("os"); + // crypto can be absent on reduced node installations + let _crypto: typeof import("crypto") | undefined; + try { + _crypto = require("crypto"); + } + catch { + _crypto = undefined; + } + let activeSession: import("inspector").Session | "stopping" | undefined; + let profilePath = "./profile.cpuprofile"; + + const isMacOs = process.platform === "darwin"; + const isLinuxOrMacOs = process.platform === "linux" || isMacOs; + + const statSyncOptions = { throwIfNoEntry: false } as const; + + const platform: string = _os.platform(); + const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + const fsRealpath = !!_fs.realpathSync.native ? process.platform === "win32" ? fsRealPathHandlingLongPath : _fs.realpathSync.native : _fs.realpathSync; + + // If our filename is "sys.js", then we are executing unbundled on the raw tsc output. + // In that case, simulate a faked path in the directory where a bundle would normally + // appear (e.g. the directory containing lib.*.d.ts files). + // + // Note that if we ever emit as files like cjs/mjs, this check will be wrong. + const executingFilePath = __filename.endsWith("sys.js") ? _path.join(_path.dirname(__dirname), "__fake__.js") : __filename; + + const fsSupportsRecursiveFsWatch = process.platform === "win32" || isMacOs; + const getCurrentDirectory = memoize(() => process.cwd()); + const { watchFile, watchDirectory } = createSystemWatchFunctions({ + pollingWatchFileWorker: fsWatchFileWorker, + getModifiedTime, + setTimeout, + clearTimeout, + fsWatchWorker, + useCaseSensitiveFileNames, + getCurrentDirectory, + fileSystemEntryExists, + // 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) + fsSupportsRecursiveFsWatch, + getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, + realpath, + tscWatchFile: process.env.TSC_WATCHFILE, + useNonPollingWatchers: !!process.env.TSC_NONPOLLING_WATCHER, + tscWatchDirectory: process.env.TSC_WATCHDIRECTORY, + inodeWatching: isLinuxOrMacOs, + fsWatchWithTimestamp: isMacOs, + sysLog, + }); + const nodeSystem: System = { + args: process.argv.slice(2), + newLine: _os.EOL, + useCaseSensitiveFileNames, + write(s: string): void { + process.stdout.write(s); + }, + getWidthOfTerminal() { + return process.stdout.columns; + }, + writeOutputIsTTY() { + return process.stdout.isTTY; + }, + readFile, + writeFile, + watchFile, + watchDirectory, + preferNonRecursiveWatch: !fsSupportsRecursiveFsWatch, + resolvePath: path => _path.resolve(path), + fileExists, + directoryExists, + getAccessibleFileSystemEntries, + createDirectory(directoryName: string) { + if (!nodeSystem.directoryExists(directoryName)) { + // Wrapped in a try-catch to prevent crashing if we are in a race + // with another copy of ourselves to create the same directory + try { + _fs.mkdirSync(directoryName); + } + catch (e) { + if (e.code !== "EEXIST") { + // Failed for some other reason (access denied?); still throw + throw e; + } + } + } + }, + getExecutingFilePath() { + return executingFilePath; + }, + getCurrentDirectory, + getDirectories, + getEnvironmentVariable(name: string) { + return process.env[name] || ""; + }, + readDirectory, + getModifiedTime, + setModifiedTime, + deleteFile, + createHash: _crypto ? createSHA256Hash : generateDjb2Hash, + createSHA256Hash: _crypto ? createSHA256Hash : undefined, + getMemoryUsage() { + if (global.gc) { + global.gc(); + } + return process.memoryUsage().heapUsed; + }, + getFileSize(path) { + const stat = statSync(path); + if (stat?.isFile()) { + return stat.size; + } + return 0; + }, + exit(exitCode?: number): void { + disableCPUProfiler(() => process.exit(exitCode)); + }, + enableCPUProfiler, + disableCPUProfiler, + cpuProfilingEnabled: () => !!activeSession || contains(process.execArgv, "--cpu-prof") || contains(process.execArgv, "--prof"), + realpath, + debugMode: !!process.env.NODE_INSPECTOR_IPC || !!process.env.VSCODE_INSPECTOR_OPTIONS || some(process.execArgv, arg => /^--(?:inspect|debug)(?:-brk)?(?:=\d+)?$/i.test(arg)) || !!(process as any).recordreplay, + tryEnableSourceMapsForHost() { + try { + (require("source-map-support") as typeof import("source-map-support")).install(); + } + catch { + // Could not enable source maps. + } + }, + setTimeout, + clearTimeout, + clearScreen: () => { + process.stdout.write("\x1B[2J\x1B[3J\x1B[H"); + }, + setBlocking: () => { + const handle = (process.stdout as any)?._handle as { setBlocking?: (value: boolean) => void; }; + if (handle && handle.setBlocking) { + handle.setBlocking(true); + } + }, + base64decode: input => Buffer.from(input, "base64").toString("utf8"), + base64encode: input => Buffer.from(input).toString("base64"), + require: (baseDir, moduleName) => { + try { + const modulePath = resolveJSModule(moduleName, baseDir, nodeSystem); + return { module: require(modulePath), modulePath, error: undefined }; + } + catch (error) { + return { module: undefined, modulePath: undefined, error }; + } + }, + }; + return nodeSystem; + + /** Calls fs.statSync, returning undefined if any errors are thrown */ + function statSync(path: string): import("fs").Stats | undefined { + // throwIfNoEntry is available in Node 14.17 and above, which matches our supported range. + try { + return _fs.statSync(path, statSyncOptions); + } + catch { + // This should never happen as we are passing throwIfNoEntry: false, + // but guard against this just in case (e.g. a polyfill doesn't check this flag). + return undefined; + } + } + + /** + * Uses the builtin inspector APIs to capture a CPU profile + * See https://nodejs.org/api/inspector.html#inspector_example_usage for details + */ + function enableCPUProfiler(path: string, cb: () => void) { + if (activeSession) { + cb(); + return false; + } + const inspector: typeof import("inspector") = require("inspector"); + if (!inspector || !inspector.Session) { + cb(); + return false; + } + const session = new inspector.Session(); + session.connect(); + + session.post("Profiler.enable", (enableErr) => { + if (enableErr) { + session.disconnect(); + cb(); + return; + } + session.post("Profiler.start", (startErr) => { + if (startErr) { + session.disconnect(); + cb(); + return; + } + activeSession = session; + profilePath = path; + cb(); + }); + }); + return true; + } + + /** + * Strips non-TS paths from the profile, so users with private projects shouldn't + * need to worry about leaking paths by submitting a cpu profile to us + */ + function cleanupPaths(profile: import("inspector").Profiler.Profile) { + let externalFileCounter = 0; + const remappedPaths = new Map(); + const normalizedDir = normalizeSlashes(_path.dirname(executingFilePath)); + // Windows rooted dir names need an extra `/` prepended to be valid file:/// urls + const fileUrlRoot = `file://${getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`; + for (const node of profile.nodes) { + if (node.callFrame.url) { + const url = normalizeSlashes(node.callFrame.url); + if (containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) { + node.callFrame.url = getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, createGetCanonicalFileName(useCaseSensitiveFileNames), /*isAbsolutePathAnUrl*/ true); + } + else if (!nativePattern.test(url)) { + node.callFrame.url = (remappedPaths.has(url) ? remappedPaths : remappedPaths.set(url, `external${externalFileCounter}.js`)).get(url)!; + externalFileCounter++; + } + } + } + return profile; + } + + function disableCPUProfiler(cb: () => void) { + if (activeSession && activeSession !== "stopping") { + const s = activeSession; + activeSession.post("Profiler.stop", (err, { profile }) => { + if (!err) { + if (statSync(profilePath)?.isDirectory()) { + profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`); + } + try { + _fs.mkdirSync(_path.dirname(profilePath), { recursive: true }); + } + catch { + // do nothing and ignore fallible fs operation + } + _fs.writeFileSync(profilePath, JSON.stringify(cleanupPaths(profile))); + } + activeSession = undefined; + s.disconnect(); + cb(); + }); + activeSession = "stopping"; + return true; + } + else { + cb(); + return false; + } + } + + function isFileSystemCaseSensitive(): boolean { + // win32\win64 are case insensitive platforms + if (platform === "win32" || platform === "win64") { + return false; + } + // If this file exists under a different case, we must be case-insensitve. + return !fileExists(swapCase(__filename)); + } + + /** Convert all lowercase chars to uppercase, and vice-versa */ + function swapCase(s: string): string { + return s.replace(/\w/g, ch => { + const up = ch.toUpperCase(); + return ch === up ? ch.toLowerCase() : up; + }); + } + + function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval: number): FileWatcher { + _fs.watchFile(fileName, { persistent: true, interval: pollingInterval }, fileChanged); + let eventKind: FileWatcherEventKind; + return { + close: () => _fs.unwatchFile(fileName, fileChanged), + }; + + function fileChanged(curr: import("fs").Stats, prev: import("fs").Stats) { + // previous event kind check is to ensure we recongnize the file as previously also missing when it is restored or renamed twice (that is it disappears and reappears) + // In such case, prevTime returned is same as prev time of event when file was deleted as per node documentation + const isPreviouslyDeleted = +prev.mtime === 0 || eventKind === FileWatcherEventKind.Deleted; + if (+curr.mtime === 0) { + if (isPreviouslyDeleted) { + // Already deleted file, no need to callback again + return; + } + eventKind = FileWatcherEventKind.Deleted; + } + else if (isPreviouslyDeleted) { + 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, curr.mtime); + } + } + + function fsWatchWorker( + fileOrDirectory: string, + recursive: boolean, + callback: FsWatchCallback, + ) { + // 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( + fileOrDirectory, + fsSupportsRecursiveFsWatch ? + { persistent: true, recursive: !!recursive } : { persistent: true }, + callback, + ); + } + + function readFile(fileName: string, _encoding?: string): string | undefined { + let buffer: Buffer; + try { + buffer = _fs.readFileSync(fileName); + } + catch { + return undefined; + } + let len = buffer.length; + if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) { + // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js, + // flip all byte pairs and treat as little endian. + len &= ~1; // Round down to a multiple of 2 + for (let i = 0; i < len; i += 2) { + const temp = buffer[i]; + buffer[i] = buffer[i + 1]; + buffer[i + 1] = temp; + } + return buffer.toString("utf16le", 2); + } + if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { + // Little endian UTF-16 byte order mark detected + return buffer.toString("utf16le", 2); + } + if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + // UTF-8 byte order mark detected + return buffer.toString("utf8", 3); + } + // Default is UTF-8 with no byte order mark + return buffer.toString("utf8"); + } + + function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { + // If a BOM is required, emit one + if (writeByteOrderMark) { + data = byteOrderMarkIndicator + data; + } + + let fd: number | undefined; + + try { + fd = _fs.openSync(fileName, "w"); + _fs.writeSync(fd, data, /*position*/ undefined, "utf8"); + } + finally { + if (fd !== undefined) { + _fs.closeSync(fd); + } + } + } + + function getAccessibleFileSystemEntries(path: string): FileSystemEntries { + try { + const entries = _fs.readdirSync(path || ".", { withFileTypes: true }); + const files: string[] = []; + const directories: string[] = []; + for (const dirent of entries) { + // withFileTypes is not supported before Node 10.10. + const entry = typeof dirent === "string" ? dirent : dirent.name; + + // This is necessary because on some file system node fails to exclude + // "." and "..". See https://github.com/nodejs/node/issues/4002 + if (entry === "." || entry === "..") { + continue; + } + + let stat: any; + if (typeof dirent === "string" || dirent.isSymbolicLink()) { + const name = combinePaths(path, entry); + + stat = statSync(name); + if (!stat) { + continue; + } + } + else { + stat = dirent; + } + + if (stat.isFile()) { + files.push(entry); + } + else if (stat.isDirectory()) { + directories.push(entry); + } + } + files.sort(); + directories.sort(); + return { files, directories }; + } + catch { + return emptyFileSystemEntries; + } + } + + function readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] { + return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries, realpath); + } + + function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { + const stat = statSync(path); + if (!stat) { + return false; + } + switch (entryKind) { + case FileSystemEntryKind.File: + return stat.isFile(); + case FileSystemEntryKind.Directory: + return stat.isDirectory(); + default: + return false; + } + } + + function fileExists(path: string): boolean { + return fileSystemEntryExists(path, FileSystemEntryKind.File); + } + + function directoryExists(path: string): boolean { + return fileSystemEntryExists(path, FileSystemEntryKind.Directory); + } + + function getDirectories(path: string): string[] { + return getAccessibleFileSystemEntries(path).directories.slice(); + } + + function fsRealPathHandlingLongPath(path: string): string { + return path.length < 260 ? _fs.realpathSync.native(path) : _fs.realpathSync(path); + } + + function realpath(path: string): string { + try { + return fsRealpath(path); + } + catch { + return path; + } + } + + function getModifiedTime(path: string) { + return statSync(path)?.mtime; + } + + function setModifiedTime(path: string, time: Date) { + try { + _fs.utimesSync(path, time, time); + } + catch { + return; + } + } + + function deleteFile(path: string) { + try { + return _fs.unlinkSync(path); + } + catch { + return; + } + } + + function createSHA256Hash(data: string): string { + const hash = _crypto!.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); + } + } + + let sys: System | undefined; + if (isNodeLikeSystem()) { + sys = getNodeSystem(); + } + if (sys) { + // patch writefile to create folder before writing the file + patchWriteFileEnsuringDirectory(sys); + } + return sys!; +})(); + +/** @internal @knipignore */ +export function setSys(s: System): void { + sys = s; +} + +if (sys && sys.getEnvironmentVariable) { + setCustomPollingValues(sys); + Debug.setAssertionLevel( + /^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) + ? AssertionLevel.Normal + : AssertionLevel.None, + ); +} +if (sys && sys.debugMode) { + Debug.isDebugging = true; +}