declare function setTimeout(handler: (...args: any[]) => void, timeout: number): any;
declare function clearTimeout(handle: any): void;

namespace ts {
    /**
     * 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() {
        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) => void;
    export type DirectoryWatcherCallback = (fileName: string) => void;
    /*@internal*/
    export 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 = new Date(0); // Any subsequent modification will occur after this time

    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 = createPollingIntervalBasedLevels(defaultChunkLevels);

    /* @internal */
    export 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<Levels> | 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);
        }
    }

    /* @internal */
    export function createDynamicPriorityPollingWatchFile(host: {
        getModifiedTime: NonNullable<System["getModifiedTime"]>;
        setTimeout: NonNullable<System["setTimeout"]>;
    }): HostWatchFile {
        interface WatchedFile extends ts.WatchedFile {
            isClosed?: boolean;
            unchangedPolls: number;
        }

        interface PollingIntervalQueue extends Array<WatchedFile> {
            pollingInterval: PollingInterval;
            pollIndex: number;
            pollScheduled: boolean;
        }

        const watchedFiles: WatchedFile[] = [];
        const changedFilesInLastPoll: WatchedFile[] = [];
        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: WatchedFile = {
                fileName,
                callback,
                unchangedPolls: 0,
                mtime: getModifiedTime(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 WatchedFile[] as PollingIntervalQueue;
            queue.pollingInterval = pollingInterval;
            queue.pollIndex = 0;
            queue.pollScheduled = false;
            return queue;
        }

        function pollPollingIntervalQueue(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(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(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: (WatchedFile | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) {
            // Max visit would be all elements of the queue
            let needsVisit = queue.length;
            let definedValueCopyToIndex = pollIndex;
            for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) {
                const watchedFile = queue[pollIndex];
                if (!watchedFile) {
                    continue;
                }
                else if (watchedFile.isClosed) {
                    queue[pollIndex] = undefined;
                    continue;
                }

                polled++;
                const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(watchedFile.fileName));
                if (watchedFile.isClosed) {
                    // Closed watcher as part of callback
                    queue[pollIndex] = undefined;
                }
                else 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);
                }

                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 nextDefinedValueIndex to end of queue, change queue size
                        queue.length = definedValueCopyToIndex;
                    }
                    pollIndex = 0;
                    definedValueCopyToIndex = 0;
                }
            }
        }

        function pollingIntervalQueue(pollingInterval: PollingInterval) {
            switch (pollingInterval) {
                case PollingInterval.Low:
                    return lowPollingIntervalQueue;
                case PollingInterval.Medium:
                    return mediumPollingIntervalQueue;
                case PollingInterval.High:
                    return highPollingIntervalQueue;
            }
        }

        function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) {
            pollingIntervalQueue(pollingInterval).push(file);
            scheduleNextPollIfNotAlreadyScheduled(pollingInterval);
        }

        function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) {
            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, pollingIntervalQueue(pollingInterval));
        }

        function getModifiedTime(fileName: string) {
            return host.getModifiedTime(fileName) || missingFileModifiedTime;
        }
    }

    function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile {
        // One file can have multiple watchers
        const fileWatcherCallbacks = createMultiMap<FileWatcherCallback>();
        const dirWatchers = new Map<string, DirectoryWatcher>();
        const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames);
        return nonPollingWatchFile;

        function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher {
            const filePath = toCanonicalName(fileName);
            fileWatcherCallbacks.add(filePath, callback);
            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);
                    // Some applications save a working file via rename operations
                    const callbacks = fileName && fileWatcherCallbacks.get(toCanonicalName(fileName));
                    if (callbacks) {
                        for (const fileCallback of callbacks) {
                            fileCallback(fileName, FileWatcherEventKind.Changed);
                        }
                    }
                },
                /*recursive*/ false,
                PollingInterval.Medium,
                fallbackOptions
            ) as DirectoryWatcher;
            watcher.referenceCount = 0;
            dirWatchers.set(dirPath, watcher);
            return watcher;
        }
    }

    /* @internal */
    export function createSingleFileWatcherPerName(
        watchFile: HostWatchFile,
        useCaseSensitiveFileNames: boolean
    ): HostWatchFile {
        interface SingleFileWatcher {
            watcher: FileWatcher;
            refCount: number;
        }
        const cache = new Map<string, SingleFileWatcher>();
        const callbacksCache = createMultiMap<FileWatcherCallback>();
        const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);

        return (fileName, callback, pollingInterval, options) => {
            const path = toCanonicalFileName(fileName);
            const existing = cache.get(path);
            if (existing) {
                existing.refCount++;
            }
            else {
                cache.set(path, {
                    watcher: watchFile(
                        fileName,
                        (fileName, eventKind) => forEach(
                            callbacksCache.get(path),
                            cb => cb(fileName, eventKind)
                        ),
                        pollingInterval,
                        options
                    ),
                    refCount: 1
                });
            }
            callbacksCache.add(path, callback);

            return {
                close: () => {
                    const watcher = Debug.checkDefined(cache.get(path));
                    callbacksCache.remove(path, callback);
                    watcher.refCount--;
                    if (watcher.refCount) return;
                    cache.delete(path);
                    closeFileWatcherOf(watcher);
                }
            };
        };
    }

    /**
     * Returns true if file status changed
     */
    /*@internal*/
    export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean {
        const oldTime = watchedFile.mtime.getTime();
        const newTime = modifiedTime.getTime();
        if (oldTime !== newTime) {
            watchedFile.mtime = modifiedTime;
            watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime));
            return true;
        }

        return false;
    }

    /*@internal*/
    export function getFileWatcherEventKind(oldTime: number, newTime: number) {
        return oldTime === 0
            ? FileWatcherEventKind.Created
            : newTime === 0
                ? FileWatcherEventKind.Deleted
                : FileWatcherEventKind.Changed;
    }

    /*@internal*/
    export const ignoredPaths = ["/node_modules/.", "/.git", "/.#"];

    /*@internal*/
    export let sysLog: (s: string) => void = noop; // eslint-disable-line prefer-const

    /*@internal*/
    export function setSysLog(logger: typeof sysLog) {
        sysLog = logger;
    }

    /*@internal*/
    export interface RecursiveDirectoryWatcherHost {
        watchDirectory: HostWatchDirectory;
        useCaseSensitiveFileNames: boolean;
        getCurrentDirectory: System["getCurrentDirectory"];
        getAccessibleSortedChildDirectories(path: string): readonly string[];
        directoryExists(dir: string): boolean;
        realpath(s: string): string;
        setTimeout: NonNullable<System["setTimeout"]>;
        clearTimeout: NonNullable<System["clearTimeout"]>;
    }

    /**
     * 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)
     */
    /*@internal*/
    export function createDirectoryWatcherSupportingRecursive({
        watchDirectory,
        useCaseSensitiveFileNames,
        getCurrentDirectory,
        getAccessibleSortedChildDirectories,
        directoryExists,
        realpath,
        setTimeout,
        clearTimeout
    }: RecursiveDirectoryWatcherHost): HostWatchDirectory {
        interface ChildDirectoryWatcher extends FileWatcher {
            dirName: string;
        }
        type ChildWatches = readonly ChildDirectoryWatcher[];
        interface HostDirectoryWatcher {
            watcher: FileWatcher;
            childWatches: ChildWatches;
            refCount: number;
        }

        const cache = new Map<string, HostDirectoryWatcher>();
        const callbackCache = createMultiMap<Path, { dirName: string; callback: DirectoryWatcherCallback; }>();
        const cacheToUpdateChildWatches = new Map<Path, { dirName: string; options: WatchOptions | undefined; fileNames: string[]; }>();
        let timerToUpdateChildWatches: any;

        const filePathComparer = getStringComparer(!useCaseSensitiveFileNames);
        const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames);

        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): ChildDirectoryWatcher {
            const dirPath = toCanonicalFilePath(dirName) as Path;
            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
                            invokeCallbacks(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
                };
                cache.set(dirPath, directoryWatcher);
                updateChildWatches(dirName, dirPath, options);
            }

            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);
                    directoryWatcher.refCount--;

                    if (directoryWatcher.refCount) return;

                    cache.delete(dirPath);
                    closeFileWatcherOf(directoryWatcher);
                    directoryWatcher.childWatches.forEach(closeFileWatcher);
                }
            };
        }

        type InvokeMap = ESMap<Path, string[] | true>;
        function invokeCallbacks(dirPath: Path, fileName: string): void;
        function invokeCallbacks(dirPath: Path, invokeMap: InvokeMap, fileNames: string[] | undefined): void;
        function invokeCallbacks(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!));
                    }
                }
            });
        }

        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 && directoryExists(dirName)) {
                // Schedule the update and postpone invoke for callbacks
                scheduleUpdateChildWatches(dirName, dirPath, fileName, options);
                return;
            }

            // Call the actual callbacks and remove child watches
            invokeCallbacks(dirPath, fileName);
            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);
        }

        function onTimerToUpdateChildWatches() {
            timerToUpdateChildWatches = undefined;
            sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`);
            const start = timestamp();
            const invokeMap = new Map<Path, string[]>();

            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);
                invokeCallbacks(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 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;
            let newChildWatches: ChildDirectoryWatcher[] | undefined;
            const hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
                directoryExists(parentDir) ? 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
            );
            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 (stringContains(path, searchPath)) return true;
            if (useCaseSensitiveFileNames) return false;
            return stringContains(toCanonicalFilePath(path), searchPath);
        }
    }

    /*@internal*/
    export type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined) => void;
    /*@internal*/
    export type FsWatch = (fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined) => FileWatcher;

    /*@internal*/
    export const enum FileSystemEntryKind {
        File,
        Directory,
    }

    /*@internal*/
    export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback {
        return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "");
    }

    function createFsWatchCallbackForFileWatcherCallback(
        fileName: string,
        callback: FileWatcherCallback,
        fileExists: System["fileExists"]
    ): FsWatchCallback {
        return eventName => {
            if (eventName === "rename") {
                callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted);
            }
            else {
                // Change
                callback(fileName, FileWatcherEventKind.Changed);
            }
        };
    }

    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 interface CreateSystemWatchFunctions {
        // Polling watch file
        pollingWatchFile: HostWatchFile;
        // For dynamic polling watch file
        getModifiedTime: NonNullable<System["getModifiedTime"]>;
        setTimeout: NonNullable<System["setTimeout"]>;
        clearTimeout: NonNullable<System["clearTimeout"]>;
        // For fs events :
        fsWatch: FsWatch;
        fileExists: System["fileExists"];
        useCaseSensitiveFileNames: boolean;
        getCurrentDirectory: System["getCurrentDirectory"];
        fsSupportsRecursiveFsWatch: boolean;
        directoryExists: System["directoryExists"];
        getAccessibleSortedChildDirectories(path: string): readonly string[];
        realpath(s: string): string;
        // For backward compatibility environment variables
        tscWatchFile: string | undefined;
        useNonPollingWatchers?: boolean;
        tscWatchDirectory: string | undefined;
    }

    /*@internal*/
    export function createSystemWatchFunctions({
        pollingWatchFile,
        getModifiedTime,
        setTimeout,
        clearTimeout,
        fsWatch,
        fileExists,
        useCaseSensitiveFileNames,
        getCurrentDirectory,
        fsSupportsRecursiveFsWatch,
        directoryExists,
        getAccessibleSortedChildDirectories,
        realpath,
        tscWatchFile,
        useNonPollingWatchers,
        tscWatchDirectory,
    }: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } {
        let dynamicPollingWatchFile: HostWatchFile | undefined;
        let nonPollingWatchFile: HostWatchFile | undefined;
        let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined;
        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.UseFsEvents:
                    return fsWatch(
                        fileName,
                        FileSystemEntryKind.File,
                        createFsWatchCallbackForFileWatcherCallback(fileName, callback, fileExists),
                        /*recursive*/ false,
                        pollingInterval,
                        getFallbackOptions(options)
                    );
                case WatchFileKind.UseFsEventsOnParentDirectory:
                    if (!nonPollingWatchFile) {
                        nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames);
                    }
                    return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options));
                default:
                    Debug.assertNever(watchFileKind);
            }
        }

        function ensureDynamicPollingWatchFile() {
            return dynamicPollingWatchFile ||
                (dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ 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 do not use fixed polling interval
                        { watchFile: WatchFileKind.FixedPollingInterval };
            }
        }

        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,
                    directoryExists,
                    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.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
                    };
            }
        }
    }

    /**
     * patch writefile to create folder before writing the file
     */
    /*@internal*/
    export function patchWriteFileEnsuringDirectory(sys: System) {
        // 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));
    }

    /*@internal*/
    export type BufferEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex";

    /*@internal*/
    interface NodeBuffer extends Uint8Array {
        constructor: any;
        write(str: string, encoding?: BufferEncoding): number;
        write(str: string, offset: number, encoding?: BufferEncoding): number;
        write(str: string, offset: number, length: number, encoding?: BufferEncoding): number;
        toString(encoding?: string, start?: number, end?: number): string;
        toJSON(): { type: "Buffer"; data: number[] };
        equals(otherBuffer: Uint8Array): boolean;
        compare(
            otherBuffer: Uint8Array,
            targetStart?: number,
            targetEnd?: number,
            sourceStart?: number,
            sourceEnd?: number
        ): number;
        copy(targetBuffer: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number): number;
        slice(begin?: number, end?: number): Buffer;
        subarray(begin?: number, end?: number): Buffer;
        writeUIntLE(value: number, offset: number, byteLength: number): number;
        writeUIntBE(value: number, offset: number, byteLength: number): number;
        writeIntLE(value: number, offset: number, byteLength: number): number;
        writeIntBE(value: number, offset: number, byteLength: number): number;
        readUIntLE(offset: number, byteLength: number): number;
        readUIntBE(offset: number, byteLength: number): number;
        readIntLE(offset: number, byteLength: number): number;
        readIntBE(offset: number, byteLength: number): number;
        readUInt8(offset: number): number;
        readUInt16LE(offset: number): number;
        readUInt16BE(offset: number): number;
        readUInt32LE(offset: number): number;
        readUInt32BE(offset: number): number;
        readInt8(offset: number): number;
        readInt16LE(offset: number): number;
        readInt16BE(offset: number): number;
        readInt32LE(offset: number): number;
        readInt32BE(offset: number): number;
        readFloatLE(offset: number): number;
        readFloatBE(offset: number): number;
        readDoubleLE(offset: number): number;
        readDoubleBE(offset: number): number;
        reverse(): this;
        swap16(): Buffer;
        swap32(): Buffer;
        swap64(): Buffer;
        writeUInt8(value: number, offset: number): number;
        writeUInt16LE(value: number, offset: number): number;
        writeUInt16BE(value: number, offset: number): number;
        writeUInt32LE(value: number, offset: number): number;
        writeUInt32BE(value: number, offset: number): number;
        writeInt8(value: number, offset: number): number;
        writeInt16LE(value: number, offset: number): number;
        writeInt16BE(value: number, offset: number): number;
        writeInt32LE(value: number, offset: number): number;
        writeInt32BE(value: number, offset: number): number;
        writeFloatLE(value: number, offset: number): number;
        writeFloatBE(value: number, offset: number): number;
        writeDoubleLE(value: number, offset: number): number;
        writeDoubleBE(value: number, offset: number): number;
        readBigUInt64BE?(offset?: number): bigint;
        readBigUInt64LE?(offset?: number): bigint;
        readBigInt64BE?(offset?: number): bigint;
        readBigInt64LE?(offset?: number): bigint;
        writeBigInt64BE?(value: bigint, offset?: number): number;
        writeBigInt64LE?(value: bigint, offset?: number): number;
        writeBigUInt64BE?(value: bigint, offset?: number): number;
        writeBigUInt64LE?(value: bigint, offset?: number): number;
        fill(value: string | Uint8Array | number, offset?: number, end?: number, encoding?: BufferEncoding): this;
        indexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number;
        lastIndexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number;
        entries(): IterableIterator<[number, number]>;
        includes(value: string | number | Buffer, byteOffset?: number, encoding?: BufferEncoding): boolean;
        keys(): IterableIterator<number>;
        values(): IterableIterator<number>;
    }

    /*@internal*/
    interface Buffer extends NodeBuffer { }

    // 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;
        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;
        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*/ 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*/ bufferFrom?(input: string, encoding?: string): Buffer;
        // For testing
        /*@internal*/ now?(): Date;
        /*@internal*/ require?(baseDir: string, moduleName: string): RequireResult;
    }

    export interface FileWatcher {
        close(): void;
    }

    interface DirectoryWatcher extends FileWatcher {
        referenceCount: number;
    }

    declare const require: any;
    declare const process: any;
    declare const global: any;
    declare const __filename: string;
    declare const __dirname: string;

    export function getNodeMajorVersion(): number | undefined {
        if (typeof process === "undefined") {
            return undefined;
        }
        const version: string = process.version;
        if (!version) {
            return undefined;
        }
        const dot = version.indexOf(".");
        if (dot === -1) {
            return undefined;
        }
        return parseInt(version.substring(1, dot));
    }

    // TODO: GH#18217 this is used as if it's certainly defined in many places.
    // eslint-disable-next-line prefer-const
    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[\\/]|[a-zA-Z0-9_\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 Buffer: {
                new (input: string, encoding?: string): any;
                from?(input: string, encoding?: string): any;
            } = require("buffer").Buffer;

            const nodeVersion = getNodeMajorVersion();
            const isNode4OrLater = nodeVersion! >= 4;
            const isLinuxOrMacOs = process.platform === "linux" || process.platform === "darwin";

            const platform: string = _os.platform();
            const useCaseSensitiveFileNames = isFileSystemCaseSensitive();
            const realpathSync = useCaseSensitiveFileNames ? (_fs.realpathSync.native ?? _fs.realpathSync) : _fs.realpathSync;

            const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin");
            const getCurrentDirectory = memoize(() => process.cwd());
            const { watchFile, watchDirectory } = createSystemWatchFunctions({
                pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames),
                getModifiedTime,
                setTimeout,
                clearTimeout,
                fsWatch,
                useCaseSensitiveFileNames,
                getCurrentDirectory,
                fileExists,
                // 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,
                directoryExists,
                getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories,
                realpath,
                tscWatchFile: process.env.TSC_WATCHFILE,
                useNonPollingWatchers: process.env.TSC_NONPOLLING_WATCHER,
                tscWatchDirectory: process.env.TSC_WATCHDIRECTORY,
            });
            const nodeSystem: System = {
                args: process.argv.slice(2),
                newLine: _os.EOL,
                useCaseSensitiveFileNames,
                write(s: string): void {
                    process.stdout.write(s);
                },
                writeOutputIsTTY() {
                    return process.stdout.isTTY;
                },
                readFile,
                writeFile,
                watchFile,
                watchDirectory,
                resolvePath: path => _path.resolve(path),
                fileExists,
                directoryExists,
                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 __filename;
                },
                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) {
                    try {
                        const stat = statSync(path);
                        if (stat?.isFile()) {
                            return stat.size;
                        }
                    }
                    catch { /*ignore*/ }
                    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(<string[]>process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)),
                tryEnableSourceMapsForHost() {
                    try {
                        require("source-map-support").install();
                    }
                    catch {
                        // Could not enable source maps.
                    }
                },
                setTimeout,
                clearTimeout,
                clearScreen: () => {
                    process.stdout.write("\x1Bc");
                },
                setBlocking: () => {
                    if (process.stdout && process.stdout._handle && process.stdout._handle.setBlocking) {
                        process.stdout._handle.setBlocking(true);
                    }
                },
                bufferFrom,
                base64decode: input => bufferFrom(input, "base64").toString("utf8"),
                base64encode: input => bufferFrom(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;

            /**
             * `throwIfNoEntry` was added so recently that it's not in the node types.
             * This helper encapsulates the mitigating usage of `any`.
             * See https://github.com/nodejs/node/pull/33716
             */
            function statSync(path: string): import("fs").Stats | undefined {
                // throwIfNoEntry will be ignored by older versions of node
                return (_fs as any).statSync(path, { throwIfNoEntry: false });
            }

            /**
             * 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<string, string>();
                const normalizedDir = normalizeSlashes(__dirname);
                // 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) {
                            try {
                                if (statSync(profilePath)?.isDirectory()) {
                                    profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
                                }
                            }
                            catch {
                                // do nothing and ignore fallible fs operation
                            }
                            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 bufferFrom(input: string, encoding?: string): Buffer {
                // See https://github.com/Microsoft/TypeScript/issues/25652
                return Buffer.from && (Buffer.from as Function) !== Int8Array.from
                    ? Buffer.from(input, encoding)
                    : new Buffer(input, encoding);
            }

            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: any, prev: any) {
                    // 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);
                }
            }

            function fsWatch(
                fileOrDirectory: string,
                entryKind: FileSystemEntryKind,
                callback: FsWatchCallback,
                recursive: boolean,
                fallbackPollingInterval: PollingInterval,
                fallbackOptions: WatchOptions | undefined
            ): FileWatcher {
                let options: any;
                let lastDirectoryPartWithDirectorySeparator: string | undefined;
                let lastDirectoryPart: string | undefined;
                if (isLinuxOrMacOs) {
                    lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substr(fileOrDirectory.lastIndexOf(directorySeparator));
                    lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(directorySeparator.length);
                }
                /** Watcher for the file system entry depending on whether it is missing or present */
                let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ?
                    watchMissingFileSystemEntry() :
                    watchPresentFileSystemEntry();
                return {
                    close: () => {
                        // Close the watcher (either existing file system entry watcher or missing file system entry watcher)
                        watcher.close();
                        watcher = undefined!;
                    }
                };

                /**
                 * Invoke the callback with rename and update the watcher if not closed
                 * @param createWatcher
                 */
                function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher) {
                    sysLog(`sysLog:: ${fileOrDirectory}:: Changing watcher to ${createWatcher === watchPresentFileSystemEntry ? "Present" : "Missing"}FileSystemEntryWatcher`);
                    // Call the callback for current directory
                    callback("rename", "");

                    // If watcher is not closed, update it
                    if (watcher) {
                        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 {
                    // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
                    // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
                    if (options === undefined) {
                        if (fsSupportsRecursiveFsWatch) {
                            options = { persistent: true, recursive: !!recursive };
                        }
                        else {
                            options = { persistent: true };
                        }
                    }
                    try {
                        const presentWatcher = _fs.watch(
                            fileOrDirectory,
                            options,
                            isLinuxOrMacOs ?
                                callbackChangingToMissingFileSystemEntry :
                                callback
                        );
                        // Watch the missing file or directory or error
                        presentWatcher.on("error", () => invokeCallbackAndUpdateWatcher(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
                        return watchPresentFileSystemEntryWithFsWatchFile();
                    }
                }

                function callbackChangingToMissingFileSystemEntry(event: "rename" | "change", relativeName: string | undefined) {
                    // 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
                    return event === "rename" &&
                        (!relativeName ||
                            relativeName === lastDirectoryPart ||
                            (relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) !== -1 && relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) === relativeName.length - lastDirectoryPartWithDirectorySeparator!.length)) &&
                        !fileSystemEntryExists(fileOrDirectory, entryKind) ?
                        invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry) :
                        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 {
                    sysLog(`sysLog:: ${fileOrDirectory}:: Changing to fsWatchFile`);
                    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) => {
                            if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) {
                                // 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
                                invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry);
                            }
                        },
                        fallbackPollingInterval,
                        fallbackOptions
                    );
                }
            }

            function readFileWorker(fileName: string, _encoding?: string): string | undefined {
                let buffer: Buffer;
                try {
                    buffer = _fs.readFileSync(fileName);
                }
                catch (e) {
                    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 readFile(fileName: string, _encoding?: string): string | undefined {
                perfLogger.logStartReadFile(fileName);
                const file = readFileWorker(fileName, _encoding);
                perfLogger.logStopReadFile();
                return file;
            }

            function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void {
                perfLogger.logEvent("WriteFile: " + fileName);
                // 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 {
                perfLogger.logEvent("ReadDir: " + (path || "."));
                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);

                            try {
                                stat = statSync(name);
                                if (!stat) {
                                    continue;
                                }
                            }
                            catch (e) {
                                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 (e) {
                    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 {
                // Since the error thrown by fs.statSync isn't used, we can avoid collecting a stack trace to improve
                // the CPU time performance.
                const originalStackTraceLimit = Error.stackTraceLimit;
                Error.stackTraceLimit = 0;

                try {
                    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;
                    }
                }
                catch (e) {
                    return false;
                }
                finally {
                    Error.stackTraceLimit = originalStackTraceLimit;
                }
            }

            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 realpath(path: string): string {
                try {
                    return realpathSync(path);
                }
                catch {
                    return path;
                }
            }

            function getModifiedTime(path: string) {
                try {
                    return statSync(path)?.mtime;
                }
                catch (e) {
                    return undefined;
                }
            }

            function setModifiedTime(path: string, time: Date) {
                try {
                    _fs.utimesSync(path, time, time);
                }
                catch (e) {
                    return;
                }
            }

            function deleteFile(path: string) {
                try {
                    return _fs.unlinkSync(path);
                }
                catch (e) {
                    return;
                }
            }

            function createSHA256Hash(data: string): string {
                const hash = _crypto!.createHash("sha256");
                hash.update(data);
                return hash.digest("hex");
            }
        }

        let sys: System | undefined;
        if (typeof process !== "undefined" && process.nextTick && !process.browser && typeof require !== "undefined") {
            // process and process.nextTick checks if current environment is node-like
            // process.browser check excludes webpack and browserify
            sys = getNodeSystem();
        }
        if (sys) {
            // patch writefile to create folder before writing the file
            patchWriteFileEnsuringDirectory(sys);
        }
        return sys!;
    })();

    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;
    }
}
