From c17c0f6bf7025f8e8c64eee81f507d16cdbdb746 Mon Sep 17 00:00:00 2001
From: Paul Miller <paul@paulmillr.com>
Date: Wed, 31 Jul 2024 23:29:31 +0000
Subject: [PATCH] Massive rewrite

---
 package.json          |   14 +-
 src/anymatch.ts       |   71 +-
 src/index.ts          |   31 +-
 src/nodefs-handler.ts |   45 +-
 src/v4.ts             | 1201 +++++++++++++++++++++++
 test-v4.mjs           | 2138 +++++++++++++++++++++++++++++++++++++++++
 tsconfig.json         |    2 +-
 7 files changed, 3395 insertions(+), 107 deletions(-)
 create mode 100644 src/v4.ts
 create mode 100644 test-v4.mjs

diff --git a/package.json b/package.json
index 541abd76..7ecbff8b 100644
--- a/package.json
+++ b/package.json
@@ -12,18 +12,14 @@
     "node": ">= 18"
   },
   "type": "module",
-  "main": "lib/index.js",
+  "main": "lib/v4.js",
   "dependencies": {
-    "is-binary-path": "2.1.0",
-    "normalize-path": "3.0.0",
-    "readdirp": "github:paulmillr/readdirp"
   },
   "devDependencies": {
     "@eslint/js": "^9.3.0",
     "@types/node": "20.12.12",
     "chai": "4.3.4",
     "eslint": "^8.57.0",
-    "globals": "^15.3.0",
     "rimraf": "5.0.5",
     "sinon": "12.0.1",
     "sinon-chai": "3.7.0",
@@ -32,8 +28,8 @@
     "upath": "2.0.1"
   },
   "files": [
-    "lib/*.js",
-    "lib/*.d.ts"
+    "lib/v4.js",
+    "lib/v4.d.ts"
   ],
   "repository": {
     "type": "git",
@@ -45,8 +41,8 @@
   "license": "MIT",
   "scripts": {
     "build": "tsc",
-    "lint": "eslint .",
-    "test": "npm run build && npm run lint && node --test"
+    "lint": "eslint src/v4.ts",
+    "test": "npm run build && npm run lint && node --test test-v4.mjs"
   },
   "keywords": [
     "fs",
diff --git a/src/anymatch.ts b/src/anymatch.ts
index 43278ca3..e9dd14d5 100644
--- a/src/anymatch.ts
+++ b/src/anymatch.ts
@@ -1,51 +1,35 @@
 import normalizePath from 'normalize-path';
 import path from 'node:path';
-import type {Stats} from 'node:fs';
+import type { Stats } from 'node:fs';
 
 export type MatchFunction = (val: string, stats?: Stats) => boolean;
 export interface MatcherObject {
   path: string;
   recursive?: boolean;
 }
-export type Matcher =
-  | string
-  | RegExp
-  | MatchFunction
-  | MatcherObject;
+export type Matcher = string | RegExp | MatchFunction | MatcherObject;
 
 function arrify<T>(item: T | T[]): T[] {
   return Array.isArray(item) ? item : [item];
 }
 
 export const isMatcherObject = (matcher: Matcher): matcher is MatcherObject =>
-    typeof matcher === 'object' &&
-      matcher !== null &&
-      !(matcher instanceof RegExp);
+  typeof matcher === 'object' && matcher !== null && !(matcher instanceof RegExp);
 
 /**
  * @param {AnymatchPattern} matcher
  * @returns {MatchFunction}
  */
 const createPattern = (matcher: Matcher): MatchFunction => {
-  if (typeof matcher === 'function') {
-    return matcher;
-  }
-  if (typeof matcher === 'string') {
-    return (string) => matcher === string;
-  }
-  if (matcher instanceof RegExp) {
-    return (string) => matcher.test(string);
-  }
+  if (typeof matcher === 'function') return matcher;
+  if (typeof matcher === 'string') return (string) => matcher === string;
+  if (matcher instanceof RegExp) return (string) => matcher.test(string);
   if (typeof matcher === 'object' && matcher !== null) {
     return (string) => {
-      if (matcher.path === string) {
-        return true;
-      }
+      if (matcher.path === string) return true;
       if (matcher.recursive) {
         const relative = path.relative(matcher.path, string);
-        if (!relative) {
-          return false;
-        }
+        if (!relative) return false;
         return !relative.startsWith('..') && !path.isAbsolute(relative);
       }
       return false;
@@ -60,20 +44,12 @@ const createPattern = (matcher: Matcher): MatchFunction => {
  * @param {Boolean} returnIndex
  * @returns {boolean|number}
  */
-function matchPatterns(
-  patterns: MatchFunction[],
-  testString: string,
-  stats?: Stats
-): boolean {
+function matchPatterns(patterns: MatchFunction[], testString: string, stats?: Stats): boolean {
   const path = normalizePath(testString);
-
   for (let index = 0; index < patterns.length; index++) {
     const pattern = patterns[index];
-    if (pattern(path, stats)) {
-      return true;
-    }
+    if (pattern(path, stats)) return true;
   }
-
   return false;
 }
 
@@ -83,34 +59,19 @@ function matchPatterns(
  * @param {object} options
  * @returns {boolean|number|Function}
  */
-function anymatch(
-  matchers: Matcher[],
-  testString: undefined
-): MatchFunction;
-function anymatch(
-  matchers: Matcher[],
-  testString: string
-): boolean;
-function anymatch(
-  matchers: Matcher[],
-  testString: string|undefined
-): boolean|MatchFunction {
-  if (matchers == null) {
-    throw new TypeError('anymatch: specify first argument');
-  }
-
+function anymatch(matchers: Matcher[], testString: undefined): MatchFunction;
+function anymatch(matchers: Matcher[], testString: string): boolean;
+function anymatch(matchers: Matcher[], testString: string | undefined): boolean | MatchFunction {
+  if (matchers == null) throw new TypeError('anymatch: specify first argument');
   // Early cache for matchers.
   const matchersArray = arrify(matchers);
-  const patterns = matchersArray
-    .map(matcher => createPattern(matcher));
-
+  const patterns = matchersArray.map((matcher) => createPattern(matcher));
   if (testString == null) {
     return (testString: string, stats?: Stats): boolean => {
       return matchPatterns(patterns, testString, stats);
     };
   }
-
   return matchPatterns(patterns, testString);
 }
 
-export {anymatch};
+export { anymatch };
diff --git a/src/index.ts b/src/index.ts
index 918f14c2..ea36c17a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,7 +2,7 @@ import fs from 'node:fs';
 import { EventEmitter } from 'node:events';
 import sysPath from 'node:path';
 import readdirp from 'readdirp';
-import {stat, readdir} from 'node:fs/promises';
+import { stat, readdir } from 'node:fs/promises';
 
 import NodeFsHandler from './nodefs-handler.js';
 import { anymatch, MatchFunction, isMatcherObject, Matcher } from './anymatch.js';
@@ -83,12 +83,8 @@ const normalizeIgnored =
   };
 
 const getAbsolutePath = (path, cwd) => {
-  if (sysPath.isAbsolute(path)) {
-    return path;
-  }
-  if (path.startsWith(BANG)) {
-    return BANG + sysPath.join(cwd, path.slice(1));
-  }
+  if (sysPath.isAbsolute(path)) return path;
+  if (path.startsWith('!')) return '!' + sysPath.join(cwd, path.slice(1)); // '!' == './'
   return sysPath.join(cwd, path);
 };
 
@@ -112,7 +108,7 @@ class DirEntry {
   add(item) {
     const { items } = this;
     if (!items) return;
-    if (item !== ONE_DOT && item !== TWO_DOTS) items.add(item);
+    if (item !== '.' && item !== '..') items.add(item);
   }
 
   async remove(item) {
@@ -169,10 +165,11 @@ export class WatchHelper {
   constructor(path: string, follow: boolean, fsw: any) {
     this.fsw = fsw;
     const watchPath = path;
-    this.path = path = path.replace(REPLACER_RE, EMPTY_STR);
+    this.path = path = path.replace(REPLACER_RE, '');
     this.watchPath = watchPath;
     this.fullWatchPath = sysPath.resolve(watchPath);
     /** @type {object|boolean} */
+
     this.dirParts = [];
     this.dirParts.forEach((parts) => {
       if (parts.length > 1) parts.pop();
@@ -182,12 +179,13 @@ export class WatchHelper {
   }
 
   entryPath(entry) {
+    // basically sysPath.absolute
     return sysPath.join(this.watchPath, sysPath.relative(this.watchPath, entry.fullPath));
   }
 
   filterPath(entry) {
     const { stats } = entry;
-    if (stats && stats.isSymbolicLink()) return this.filterDir(entry);
+    if (stats && stats.isSymbolicLink()) return this.filterDir(entry); /// WUT?! symlink can be file too
     const resolvedPath = this.entryPath(entry);
     return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
   }
@@ -345,6 +343,7 @@ export class FSWatcher extends EventEmitter {
     }
     if (opts.ignored) opts.ignored = arrify(opts.ignored);
 
+    // Done to emit ready only once, but each 'add' will increase that
     let readyCalls = 0;
     this._emitReady = () => {
       readyCalls++;
@@ -472,6 +471,7 @@ export class FSWatcher extends EventEmitter {
 
       this._closePath(path);
 
+      // TWICE!
       this._addIgnoredPath(path);
       if (this._watched.has(path)) {
         this._addIgnoredPath({
@@ -507,7 +507,7 @@ export class FSWatcher extends EventEmitter {
     );
     this._streams.forEach((stream) => stream.destroy());
     this._userIgnored = undefined;
-    this._readyCount = 0;
+    this._readyCount = 0; // allows to re-start
     this._readyEmitted = false;
     this._watched.forEach((dirent) => dirent.dispose());
     ['closers', 'watched', 'streams', 'symlinkPaths', 'throttled'].forEach((key) => {
@@ -544,6 +544,7 @@ export class FSWatcher extends EventEmitter {
   /**
    * Normalize and emit events.
    * Calling _emit DOES NOT MEAN emit() would be called!
+   * val1 == stats, others are unused
    * @param {EventName} event Type of event
    * @param {Path} path File or directory path
    * @param {*=} val1 arguments to be passed with event
@@ -907,6 +908,7 @@ export class FSWatcher extends EventEmitter {
   _closeFile(path) {
     const closers = this._closers.get(path);
     if (!closers) return;
+    // no promise handling here
     closers.forEach((closer) => closer());
     this._closers.delete(path);
   }
@@ -931,10 +933,13 @@ export class FSWatcher extends EventEmitter {
     const options = { type: EV.ALL, alwaysStat: true, lstat: true, ...opts };
     let stream = readdirp(root, options);
     this._streams.add(stream);
-    stream.once(STR_CLOSE, () => {
+    // possible mem leak if emited before end
+    stream.once('close', () => {
+      console.log('readdirp close');
       stream = undefined;
     });
-    stream.once(STR_END, () => {
+    stream.once('end', () => {
+      console.log('readdirp end');
       if (stream) {
         this._streams.delete(stream);
         stream = undefined;
diff --git a/src/nodefs-handler.ts b/src/nodefs-handler.ts
index efbd486b..293e55c5 100644
--- a/src/nodefs-handler.ts
+++ b/src/nodefs-handler.ts
@@ -19,12 +19,7 @@ import {
 } from './constants.js';
 import * as EV from './events.js';
 import type { FSWatcher, WatchHelper, FSWInstanceOptions } from './index.js';
-import {
-  open,
-  stat,
-  lstat,
-  realpath as fsrealpath
-} from 'node:fs/promises';
+import { open, stat, lstat, realpath as fsrealpath } from 'node:fs/promises';
 
 const THROTTLE_MODE_WATCH = 'watch';
 
@@ -41,19 +36,14 @@ const foreach = (val, fn) => {
 
 const addAndConvert = (main, prop, item) => {
   let container = main[prop];
-  if (!(container instanceof Set)) {
-    main[prop] = container = new Set([container]);
-  }
+  if (!(container instanceof Set)) main[prop] = container = new Set([container]);
   container.add(item);
 };
 
 const clearItem = (cont) => (key) => {
   const set = cont[key];
-  if (set instanceof Set) {
-    set.clear();
-  } else {
-    delete cont[key];
-  }
+  if (set instanceof Set) set.clear();
+  else delete cont[key];
 };
 
 const delFromSet = (main, prop, item) => {
@@ -117,9 +107,13 @@ function createFsWatchInstance(
     }
   };
   try {
-    return fs.watch(path, {
-      persistent: options.persistent
-    }, handleEvent);
+    return fs.watch(
+      path,
+      {
+        persistent: options.persistent,
+      },
+      handleEvent
+    );
   } catch (error) {
     errHandler(error);
   }
@@ -329,7 +323,7 @@ export default class NodeFsHandler {
     parent.add(basename);
     const absolutePath = sysPath.resolve(path);
     const options: Partial<FSWInstanceOptions> = {
-      persistent: opts.persistent
+      persistent: opts.persistent,
     };
     if (!listener) listener = EMPTY_FN;
 
@@ -359,9 +353,7 @@ export default class NodeFsHandler {
    * @returns {Function} closer for the watcher instance
    */
   _handleFile(file, stats, initialAdd) {
-    if (this.fsw.closed) {
-      return;
-    }
+    if (this.fsw.closed) return;
     const dirname = sysPath.dirname(file);
     const basename = sysPath.basename(file);
     const parent = this.fsw._getWatchedDir(dirname);
@@ -372,12 +364,10 @@ export default class NodeFsHandler {
     if (parent.has(basename)) return;
 
     const listener = async (path, newStats) => {
-      console.log({path, newStats});
       if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5)) return;
       if (!newStats || newStats.mtimeMs === 0) {
         try {
           const newStats = await stat(file);
-          console.log({newStats, prevStats});
           if (this.fsw.closed) return;
           // Check that change event was not fired because of changed only accessTime.
           const at = newStats.atimeMs;
@@ -393,7 +383,6 @@ export default class NodeFsHandler {
             prevStats = newStats;
           }
         } catch (error) {
-          console.log({error});
           // Fix issues where mtime is null but file is still present
           this.fsw._remove(dirname, basename);
         }
@@ -536,10 +525,7 @@ export default class NodeFsHandler {
         previous
           .getChildren()
           .filter((item) => {
-            return (
-              item !== directory &&
-              !current.has(item)
-            );
+            return item !== directory && !current.has(item);
           })
           .forEach((item) => {
             this.fsw._remove(directory, item);
@@ -568,6 +554,7 @@ export default class NodeFsHandler {
     const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir));
     const tracked = parentDir.has(sysPath.basename(dir));
     if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
+      console.log('addDir', dir, new Error().stack);
       this.fsw._emit(EV.ADD_DIR, dir, stats);
     }
 
@@ -604,7 +591,7 @@ export default class NodeFsHandler {
    * @param {String=} target Child path actually targeted for watch
    * @returns {Promise}
    */
-  async _addToNodeFs(path, initialAdd, priorWh: WatchHelper|undefined, depth, target?: string) {
+  async _addToNodeFs(path, initialAdd, priorWh: WatchHelper | undefined, depth, target?: string) {
     const ready = this.fsw._emitReady;
     if (this.fsw._isIgnored(path) || this.fsw.closed) {
       ready();
diff --git a/src/v4.ts b/src/v4.ts
new file mode 100644
index 00000000..05af71fb
--- /dev/null
+++ b/src/v4.ts
@@ -0,0 +1,1201 @@
+import fs from 'node:fs';
+import { EventEmitter } from 'node:events';
+import sysPath from 'node:path';
+import { readdir, lstat, open, stat, realpath as fsrealpath } from 'node:fs/promises';
+import { type as osType } from 'os';
+import type { BigIntStats, Stats as NodeStats } from 'node:fs';
+// readlink
+// Platform information
+const { platform } = process;
+export const isWindows = platform === 'win32';
+export const isMacos = platform === 'darwin';
+export const isLinux = platform === 'linux';
+export const isIBMi = osType() === 'OS400';
+// prettier-ignore
+const binaryExtensions = new Set([
+  '3dm', '3ds', '3g2', '3gp', '7z', 'a', 'aac', 'adp', 'afdesign', 'afphoto', 'afpub', 'ai',
+  'aif', 'aiff', 'alz', 'ape', 'apk', 'appimage', 'ar', 'arj', 'asf', 'au', 'avi',
+  'bak', 'baml', 'bh', 'bin', 'bk', 'bmp', 'btif', 'bz2', 'bzip2',
+  'cab', 'caf', 'cgm', 'class', 'cmx', 'cpio', 'cr2', 'cur', 'dat', 'dcm', 'deb', 'dex', 'djvu',
+  'dll', 'dmg', 'dng', 'doc', 'docm', 'docx', 'dot', 'dotm', 'dra', 'DS_Store', 'dsk', 'dts',
+  'dtshd', 'dvb', 'dwg', 'dxf',
+  'ecelp4800', 'ecelp7470', 'ecelp9600', 'egg', 'eol', 'eot', 'epub', 'exe',
+  'f4v', 'fbs', 'fh', 'fla', 'flac', 'flatpak', 'fli', 'flv', 'fpx', 'fst', 'fvt',
+  'g3', 'gh', 'gif', 'graffle', 'gz', 'gzip',
+  'h261', 'h263', 'h264', 'icns', 'ico', 'ief', 'img', 'ipa', 'iso',
+  'jar', 'jpeg', 'jpg', 'jpgv', 'jpm', 'jxr', 'key', 'ktx',
+  'lha', 'lib', 'lvp', 'lz', 'lzh', 'lzma', 'lzo',
+  'm3u', 'm4a', 'm4v', 'mar', 'mdi', 'mht', 'mid', 'midi', 'mj2', 'mka', 'mkv', 'mmr','mng',
+  'mobi', 'mov', 'movie', 'mp3',
+  'mp4', 'mp4a', 'mpeg', 'mpg', 'mpga', 'mxu',
+  'nef', 'npx', 'numbers', 'nupkg',
+  'o', 'odp', 'ods', 'odt', 'oga', 'ogg', 'ogv', 'otf', 'ott',
+  'pages', 'pbm', 'pcx', 'pdb', 'pdf', 'pea', 'pgm', 'pic', 'png', 'pnm', 'pot', 'potm',
+  'potx', 'ppa', 'ppam',
+  'ppm', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx', 'psd', 'pya', 'pyc', 'pyo', 'pyv',
+  'qt',
+  'rar', 'ras', 'raw', 'resources', 'rgb', 'rip', 'rlc', 'rmf', 'rmvb', 'rpm', 'rtf', 'rz',
+  's3m', 's7z', 'scpt', 'sgi', 'shar', 'snap', 'sil', 'sketch', 'slk', 'smv', 'snk', 'so',
+  'stl', 'suo', 'sub', 'swf',
+  'tar', 'tbz', 'tbz2', 'tga', 'tgz', 'thmx', 'tif', 'tiff', 'tlz', 'ttc', 'ttf', 'txz',
+  'udf', 'uvh', 'uvi', 'uvm', 'uvp', 'uvs', 'uvu',
+  'viv', 'vob',
+  'war', 'wav', 'wax', 'wbmp', 'wdp', 'weba', 'webm', 'webp', 'whl', 'wim', 'wm', 'wma',
+  'wmv', 'wmx', 'woff', 'woff2', 'wrm', 'wvx',
+  'xbm', 'xif', 'xla', 'xlam', 'xls', 'xlsb', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx', 'xm',
+  'xmind', 'xpi', 'xpm', 'xwd', 'xz',
+  'z', 'zip', 'zipx',
+]);
+const isBinaryPath = (filePath) =>
+  binaryExtensions.has(sysPath.extname(filePath).slice(1).toLowerCase());
+
+// Small internal primitive to limit concurrency
+// TODO: identify potential bugs. Research hpw other libraries do this
+function limit(concurrencyLimit?: number) {
+  if (concurrencyLimit === undefined) return <T>(fn: () => T): T => fn(); // Fast path for no limit
+  let currentlyProcessing = 0;
+  const queue: ((value?: unknown) => void)[] = [];
+  const next = () => {
+    if (!queue.length) return;
+    if (currentlyProcessing >= concurrencyLimit) return;
+    currentlyProcessing++;
+    const first = queue.shift();
+    if (!first) throw new Error('empty queue'); // should not happen
+    first();
+  };
+  return <T>(fn: () => Promise<T>): Promise<T> =>
+    new Promise<T>((resolve, reject) => {
+      queue.push(() =>
+        Promise.resolve()
+          .then(fn)
+          .then(resolve)
+          .catch(reject)
+          .finally(() => {
+            currentlyProcessing--;
+            next();
+          })
+      );
+      next();
+    });
+}
+
+// prettier-ignore
+type EventName = 'all' | 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir' | 'raw' | 'error' | 'ready';
+type Stats = NodeStats | BigIntStats;
+type Path = string;
+type ThrottleType = 'readdir' | 'watch' | 'add' | 'remove' | 'change';
+type EmitArgs = [EventName, Path, any?, any?, any?];
+
+const arrify = <T>(value: T | T[] = []): T[] => (Array.isArray(value) ? value : [value]);
+const flatten = <T>(list: T[] | T[][], result = []): T[] => {
+  list.forEach((item) => {
+    if (Array.isArray(item)) flatten(item, result);
+    else result.push(item);
+  });
+  return result;
+};
+
+/**
+ * Check for read permissions.
+ * @param stats - object, result of fs_stat
+ * @returns indicates whether the file can be read
+ */
+function hasReadPermissions(stats: Stats) {
+  return Boolean(Number(stats.mode) & 0o400);
+}
+
+const NORMAL_FLOW_ERRORS = new Set(['ENOENT', 'EPERM', 'EACCES', 'ELOOP']);
+
+// Legacy list of user events
+export const EV = {
+  ALL: 'all',
+  READY: 'ready',
+  ADD: 'add',
+  CHANGE: 'change',
+  ADD_DIR: 'addDir',
+  UNLINK: 'unlink',
+  UNLINK_DIR: 'unlinkDir',
+  RAW: 'raw',
+  ERROR: 'error',
+};
+
+/*
+Re-usable instances of fs.watch and fs.watchFile. Architecture rationale:
+- FSWatcher can have multiple listeners here:
+  - followSymlinks + two symlinks to the same file
+  - directory + symlink inside
+  - different paths (absolute + relative) without cwd
+- Multiple FSWatcher-s can reuse the same watcher
+- This means we cannot just add Set<FSWatcher> for err/raw (it will require reference counting)
+  Should be very simple code to create watchers only, all logic and IO should be handled inside of FSWatcher
+- Returns sync 'closer' function which should ensure that no events emitted after closing.
+  This is needed for cases when the directory was moved.
+*/
+type WatchHandlers = {
+  listener: (path: string, stats?: Stats) => void;
+  errHandler: (err: Error, path?: Path, fullPath?: Path) => void;
+  rawEmitter: (ev: fs.WatchEventType, path: string, opts: unknown) => void;
+};
+
+type WatchInstancePartial = {
+  listeners: Set<WatchHandlers['listener']>;
+  errHandlers: Set<WatchHandlers['errHandler']>;
+  rawEmitters: Set<WatchHandlers['rawEmitter']>;
+  options: Partial<FSWInstanceOptions>;
+};
+
+type WatchInstance<T> = WatchInstancePartial & { watcher?: T };
+
+type WatchFn<T> = {
+  upgrade?: boolean;
+  create: (path: Path, fullPath: Path, instance: WatchInstancePartial) => T;
+  close: (path: Path, fullPath: Path, watcher?: T) => void;
+};
+
+const watchWrapper = <T>(opts: WatchFn<T>) => {
+  const instances: Map<string, WatchInstance<T>> = new Map();
+  const { create, close, upgrade } = opts;
+  return (
+    path: Path,
+    fullPath: Path,
+    options: Partial<FSWInstanceOptions>,
+    handlers: WatchHandlers
+  ) => {
+    let cont: WatchInstance<T> | undefined = instances.get(fullPath);
+    const { listener, errHandler, rawEmitter } = handlers;
+    const copts = cont && cont.options;
+    // This seems like a rare case.
+    // In theory, we can upgrade 'watch' too, but instead
+    // fallback to creating non-global instance if different persistence
+    let differentOptions =
+      copts && (copts.persistent < options.persistent || copts.interval > options.interval);
+    if (upgrade && differentOptions) {
+      // "Upgrade" the watcher to persistence or a quicker interval.
+      // This creates some unlikely edge case issues if the user mixes
+      // settings in a very weird way, but solving for those cases
+      // doesn't seem worthwhile for the added complexity.
+      close(path, fullPath, cont.watcher);
+      cont = undefined;
+      differentOptions = false; // upgraded, now options are the same
+    }
+    if (!cont || differentOptions) {
+      cont = { listeners: new Set(), errHandlers: new Set(), rawEmitters: new Set(), options };
+      try {
+        cont.watcher = create(path, fullPath, cont);
+        // non-global instance if options still different
+        if (!differentOptions) instances.set(fullPath, cont);
+      } catch (error) {
+        errHandler(error);
+        return;
+      }
+    }
+    cont.listeners.add(listener);
+    cont.errHandlers.add(errHandler);
+    cont.rawEmitters.add(rawEmitter);
+    return () => {
+      cont.listeners.delete(listener);
+      cont.errHandlers.delete(errHandler);
+      cont.rawEmitters.delete(rawEmitter);
+      if (cont.listeners.size) return; // All listeners left, lets close
+      if (!differentOptions) instances.delete(fullPath); // when same options: use global
+      close(path, fullPath, cont.watcher);
+      cont.listeners.clear();
+      cont.errHandlers.clear();
+      cont.rawEmitters.clear();
+      cont.watcher = undefined;
+      Object.freeze(cont);
+    };
+  };
+};
+
+const fsWatch = watchWrapper({
+  create(path, fullPath, instance) {
+    const { options, listeners, rawEmitters, errHandlers } = instance;
+    // TODO: why it is using path instead full path?
+    return fs
+      .watch(path, { persistent: options.persistent }, (rawEvent, evPath) => {
+        for (const fn of listeners) fn(path);
+        for (const fn of rawEmitters) fn(rawEvent, evPath, { watchedPath: path });
+        // NOTE: previously there was re-emitting event "for files from a
+        // directory's watcher in case the file's watcher misses it"
+        // However, this is incorrect and can cause race-conditions if current
+        // watcher is already closed.
+        // Please open issue if there is a reproducible case for this.
+      })
+      .on('error', (err) => {
+        for (const fn of errHandlers) fn(err, path, fullPath);
+      });
+  },
+  close(path, fullPath, watcher) {
+    if (watcher) watcher.close();
+  },
+});
+
+const fsWatchFile = watchWrapper({
+  upgrade: true,
+  create(path, fullPath, instance) {
+    const { listeners, rawEmitters, options } = instance;
+    return fs.watchFile(fullPath, options, (curr, prev) => {
+      for (const rawEmitter of rawEmitters) rawEmitter('change', fullPath, { curr, prev });
+      const currmtime = curr.mtimeMs;
+      if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
+        for (const listener of listeners) listener(path, curr);
+      }
+    });
+  },
+  // eslint-disable-next-line
+  close(path, fullPath, _watcher) {
+    fs.unwatchFile(fullPath);
+  },
+});
+
+// Matcher
+type MatchFunction = (path: string, stats?: Stats) => boolean;
+interface MatcherObject {
+  path: string;
+  recursive?: boolean;
+}
+type Matcher = string | RegExp | MatchFunction | MatcherObject;
+function isMatcherObject(matcher: Matcher): matcher is MatcherObject {
+  return typeof matcher === 'object' && matcher !== null && !(matcher instanceof RegExp);
+}
+type AWF = {
+  stabilityThreshold: number;
+  pollInterval: number;
+};
+
+type BasicOpts = {
+  persistent: boolean;
+  ignoreInitial: boolean;
+  followSymlinks: boolean;
+  cwd?: string;
+  // Polling
+  usePolling: boolean;
+  interval: number;
+  binaryInterval: number; // Used only for pooling and if diferrent from interval
+
+  alwaysStat?: boolean;
+  depth?: number;
+  ignorePermissionErrors: boolean;
+  atomic: boolean | number; // or a custom 'atomicity delay', in milliseconds (default 100)
+  useAsync?: boolean; // Use async for stat/readlink methods
+
+  ioLimit?: number; // Limit parallel IO operations (CPU usage + OS limits)
+};
+
+export type ChokidarOptions = Partial<
+  BasicOpts & {
+    ignored: string | ((path: string) => boolean); // And what about regexps?
+    awaitWriteFinish: boolean | Partial<AWF>;
+  }
+>;
+
+export type FSWInstanceOptions = BasicOpts & {
+  ignored: Matcher[]; // string | fn ->
+  awaitWriteFinish: false | AWF;
+};
+
+/**
+ * Watches files & directories for changes. Emitted events:
+ * `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error`
+ *
+ *     new FSWatcher()
+ *       .add(directories)
+ *       .on('add', path => log('File', path, 'was added'))
+ */
+export class FSWatcher extends EventEmitter {
+  options: FSWInstanceOptions;
+  private watched: Map<string, Set<string>> = new Map();
+  private closers: Map<string, Array<any>> = new Map();
+  private ignoredPaths: Set<Matcher> = new Set<Matcher>();
+  private throttled: Map<ThrottleType, Map<any, any>> = new Map();
+  private symlinkPaths: Map<Path, string | boolean> = new Map();
+  closed: boolean = false;
+  private pendingWrites: Map<any, any> = new Map();
+  private pendingUnlinks: Map<any, any> = new Map();
+  private readyCount: number;
+  private emitReady: () => void;
+  private closePromise: Promise<void>;
+  private userIgnored?: MatchFunction;
+  private readyEmitted: boolean = false;
+  // Performance debug related stuff. Not sure if worth exposing in API?
+  public metrics: Record<string, number> = {};
+  private ioLimit: ReturnType<typeof limit>;
+  constructor(_opts: ChokidarOptions = {}) {
+    super();
+    const awf = _opts.awaitWriteFinish;
+    const DEF_AWF = { stabilityThreshold: 2000, pollInterval: 100 };
+    const opts: FSWInstanceOptions = {
+      // Defaults
+      persistent: true,
+      ignoreInitial: false,
+      ignorePermissionErrors: false,
+      interval: 100,
+      binaryInterval: 300,
+      followSymlinks: true,
+      usePolling: false,
+      useAsync: false,
+      atomic: true, // NOTE: overwritten later (depends on usePolling)
+      ..._opts,
+      // Change format
+      ignored: arrify(_opts.ignored),
+      awaitWriteFinish:
+        awf === true ? DEF_AWF : typeof awf === 'object' ? { ...DEF_AWF, ...awf } : false,
+    };
+    // Always default to polling on IBM i because fs.watch() is not available on IBM i.
+    if (isIBMi) opts.usePolling = true;
+    // Editor atomic write normalization enabled by default with fs.watch
+    if (opts.atomic === undefined) opts.atomic = !opts.usePolling;
+    opts.atomic = typeof _opts.atomic === 'number' ? _opts.atomic : 100;
+    // Global override. Useful for developers, who need to force polling for all
+    // instances of chokidar, regardless of usage / dependency depth
+    const envPoll = process.env.CHOKIDAR_USEPOLLING;
+    if (envPoll !== undefined) {
+      const envLower = envPoll.toLowerCase();
+      if (envLower === 'false' || envLower === '0') opts.usePolling = false;
+      else if (envLower === 'true' || envLower === '1') opts.usePolling = true;
+      else opts.usePolling = !!envLower;
+    }
+    const envInterval = process.env.CHOKIDAR_INTERVAL;
+    if (envInterval) opts.interval = Number.parseInt(envInterval, 10);
+    this.ioLimit = limit(opts.ioLimit);
+    // TODO: simplify. Currently it will easily lose things
+    // This seems done to emit ready only once, but each 'add' will increase that?
+    let readyCalls = 0;
+    this.emitReady = () => {
+      readyCalls++;
+      if (readyCalls >= this.readyCount) {
+        this.emitReady = () => {};
+        this.readyEmitted = true;
+        // use process.nextTick to allow time for listener to be bound
+        process.nextTick(() => this.emit('ready'));
+      }
+    };
+    // You’re frozen when your heart’s not open.
+    Object.freeze(opts);
+    this.options = opts;
+  }
+  // IO
+  private metric(name: string, inc = 1) {
+    if (!this.metrics[name]) this.metrics[name] = 0;
+    this.metrics[name] += inc;
+  }
+  private readdir(path: string) {
+    // dirent is available in node v18 on macos + win + linux
+    return this.ioLimit(async () => {
+      try {
+        return await readdir(path, { encoding: 'utf8', withFileTypes: true });
+      } catch (err) {
+        if (!NORMAL_FLOW_ERRORS.has(err.code)) this.handleError(err);
+        return [];
+      } finally {
+        this.metric('readdir');
+      }
+    });
+  }
+  private lstat(path: string): Promise<Stats | undefined> {
+    // Available in node v18: bigint allows access to 'mtimeNs' which has more precision than mtimeMs.
+    // It's no longer a float, which can't be compared.
+    // Also, there is no mtime/mode in DirEnt
+    return this.ioLimit(async () => {
+      try {
+        if (!this.options.useAsync) return fs.lstatSync(path, { bigint: true });
+        return await lstat(path, { bigint: true });
+      } catch (err) {
+        if (!NORMAL_FLOW_ERRORS.has(err.code)) this.handleError(err);
+        return;
+      } finally {
+        this.metric('lstat');
+      }
+    });
+  }
+  private stat(path: string): Promise<Stats | undefined> {
+    // Available in node v18: bigint allows access to 'mtimeNs' which has more precision than mtimeMs.
+    // It's no longer a float, which can't be compared.
+    // Also, there is no mtime/mode in DirEnt
+    return this.ioLimit(async () => {
+      try {
+        if (!this.options.useAsync) return fs.statSync(path, {});
+        return await stat(path, {});
+      } catch (err) {
+        if (!NORMAL_FLOW_ERRORS.has(err.code)) this.handleError(err);
+        return;
+      } finally {
+        this.metric('stat');
+      }
+    });
+  }
+  // private readlink(path: string) {
+  //   return this.ioLimit(async () => {
+  //     try {
+  //       if (!this.options.useAsync) return fs.readlinkSync(path, { encoding: 'utf8' });
+  //       return await readlink(path, { encoding: 'utf8' });
+  //     } catch (err) {
+  //       if (!NORMAL_FLOW_ERRORS.has(err.code)) this.handleError(err);
+  //       return;
+  //     } finally {
+  //       this.metric('readlink');
+  //     }
+  //   });
+  // }
+  private canOpen(path: string) {
+    return this.ioLimit(async () => {
+      try {
+        if (!this.options.useAsync) {
+          fs.closeSync(fs.openSync(path, 'r'));
+        } else {
+          await (await open(path, 'r')).close();
+        }
+      } catch (err) {
+        return false;
+      } finally {
+        this.metric('canOpen');
+      }
+      return true;
+    });
+  }
+  // This mostly happens after removing file. Checks if directory still exists.
+  // TODO: either use real readdir (with updating information) or remove this
+  private async canOpenDir(dir: Path) {
+    dir = sysPath.resolve(dir);
+    return this.ioLimit(async () => {
+      const items = this.getWatchedDir(dir);
+      if (items.size > 0) return;
+      try {
+        await readdir(dir);
+      } catch (err) {
+        this.remove(sysPath.dirname(dir), sysPath.basename(dir));
+      }
+    });
+  }
+  // /IO
+  // Utils
+  /**
+   * Provides directory tracking objects
+   */
+  private getWatchedDir(directory: string): Set<string> {
+    const dir = sysPath.resolve(directory);
+    if (!this.watched.has(dir)) this.watched.set(dir, new Set());
+    return this.watched.get(dir);
+  }
+  private normalizePath(path: Path) {
+    const { cwd } = this.options;
+    path = sysPath.normalize(path);
+    // TODO: do we really need that? only thing it does is using '//' instead of '\\' for network shares
+    // in windows. Path normalize already strips '//' in windows.
+    // > If SLASH_SLASH occurs at the beginning of path, it is not replaced
+    // >    because "//StoragePC/DrivePool/Movies" is a valid network path
+    path = path.replace(/\\/g, '/');
+    let prepend = false;
+    if (path.startsWith('//')) prepend = true;
+    const DOUBLE_SLASH_RE = /\/\//;
+    while (path.match(DOUBLE_SLASH_RE)) path = path.replace(DOUBLE_SLASH_RE, '/');
+    if (prepend) path = '/' + path;
+    // NOTE: join will undo all normalization
+    if (cwd) path = sysPath.isAbsolute(path) ? path : sysPath.join(cwd, path);
+    return path;
+  }
+  private normalizePaths(paths: Path | Path[]) {
+    // TODO: do we really need flatten here?
+    paths = flatten(arrify(paths));
+    if (!paths.every((p) => typeof p === 'string'))
+      throw new TypeError(`Non-string provided as watch path: ${paths}`);
+    return paths.map((i) => this.normalizePath(i));
+  }
+  /**
+   * Helper utility for throttling
+   * @param actionType type being throttled
+   * @param path being acted upon
+   * @param ms duration to suppress duplicate actions
+   * @returns tracking object or false if action should be suppressed
+   */
+  private throttle(actionType: ThrottleType, path: Path, ms: number) {
+    // NOTE: this is only used correctly in readdir for now.
+    // How it should work:
+    // - we process some event
+    // - same event happens in parallel multiple times
+    // - when we processed first event, we look at last throttled event
+    // - start processing it
+    // How it works now (except readdir):
+    // - we process first event (first change)
+    // - there is multiple parallel changes which we throw away
+    // - all changes which happened when we processed first event is lost
+    if (!this.throttled.has(actionType)) this.throttled.set(actionType, new Map());
+    const action = this.throttled.get(actionType);
+    const actionPath = action.get(path);
+    if (actionPath) {
+      actionPath.count++;
+      return false;
+    }
+    const thr = {
+      ms,
+      timeout: undefined,
+      count: 0,
+      clear: () => {
+        const item = action.get(path);
+        const count = item ? item.count : 0;
+        action.delete(path);
+        if (item) {
+          if (item.timeout !== undefined) clearTimeout(item.timeout);
+          item.timeout = undefined;
+        }
+        if (thr.timeout !== undefined) clearTimeout(thr.timeout);
+        thr.timeout = undefined;
+        return count;
+      },
+    };
+    thr.timeout = setTimeout(thr.clear, ms);
+    action.set(path, thr);
+    return thr;
+  }
+  // /Utils
+  // Watcher
+  //
+  private addWatcher(closerPath: Path, path: Path, listener: WatchHandlers['listener']) {
+    const opts = this.options;
+    const directory = sysPath.dirname(path);
+    const basename = sysPath.basename(path);
+    this.getWatchedDir(directory).add(basename);
+    const absolutePath = sysPath.resolve(path);
+    const options: Partial<FSWInstanceOptions> = {
+      persistent: opts.persistent,
+      interval:
+        opts.binaryInterval !== opts.interval && isBinaryPath(basename)
+          ? opts.binaryInterval
+          : opts.interval,
+    };
+    const fn = opts.usePolling ? fsWatchFile : fsWatch;
+    return this.ioLimit(async () => {
+      const closer = fn(path, absolutePath, options, {
+        listener: listener as any,
+        errHandler: (error: Error, path?: Path) => this.handleError(error, path),
+        rawEmitter: (...args) => this.emit('raw', ...args),
+      });
+      if (closer) {
+        //  closerPath = sysPath.resolve(closerPath);
+        const list = this.closers.get(closerPath);
+        if (!list) this.closers.set(closerPath, [closer]);
+        else list.push(closer);
+      }
+    });
+  }
+  private closeWatcher(path: Path) {
+    //path = sysPath.resolve(path);
+    const closers = this.closers.get(path);
+    if (closers) {
+      for (const closer of closers) closer();
+      this.closers.delete(path);
+    }
+    const dirname = sysPath.dirname(path);
+    const dir = this.getWatchedDir(dirname);
+    dir.delete(sysPath.basename(path));
+    this.canOpenDir(dirname);
+  }
+  // /Watcher
+
+  /**
+   * Handle added file, directory, or glob pattern.
+   * Delegates call to handleFile / _handleDir after checks.
+   * @param {String} path to file or ir
+   * @param {Boolean} initialAdd was the file added at watch instantiation?
+   * @param {Object} priorWh depth relative to user-supplied path
+   * @param {Number} depth Child path actually targeted for watch
+   * @param {String=} target Child path actually targeted for watch
+   * @returns {Promise}
+   */
+  private async addToNodeFs(
+    path: Path,
+    initialAdd: boolean,
+    priorWh: string | undefined,
+    depth: number,
+    target?: string
+  ) {
+    // TODO: this is completely messed up
+    // - we need to use dirent from readdir on recursive call to itself
+    // - instead of handling symlinks/stats in single place it does
+    //   it multiple times (in readdir, then same thing happens inside recursive addToNodeFs)
+    //   - what makes this even worse, some edge cases handled in 'file', others in 'readdir'
+    //     this means add('dir/file') has different behavior than add('dir') (and then looking at 'file')
+    // - symlinks handling is broken abomination which is also intervened with broken normalization and path handling
+    // - emitReady should be Promise.all on 'addWait' which returns when everything added
+    //   instead of randomly placed 'readyCount'
+    // - these 200 lines should be collapsed to 30-50
+    if (this.isIgnored(path) || this.closed) {
+      this.emitReady();
+      return false;
+    }
+    const watchPath = priorWh ? priorWh : path;
+    const entryPath = (fullPath) => sysPath.join(watchPath, sysPath.relative(watchPath, fullPath));
+    // evaluate what is at the path we're being asked to watch
+    try {
+      const follow = this.options.followSymlinks;
+      const stats = await (follow ? this.stat(path) : this.lstat(path)); // TODO: this creates more calls when done inside of a directory
+      if (this.closed) return;
+      if (this.isIgnored(path, stats)) {
+        this.emitReady();
+        return false;
+      }
+      const _handleDir = async (closerPath, dir, target, realpath) => {
+        const parentDir = this.getWatchedDir(sysPath.dirname(dir));
+        const tracked = parentDir.has(sysPath.basename(dir));
+        if (!(initialAdd && this.options.ignoreInitial) && !target && !tracked)
+          this._emit('addDir', dir, stats);
+        const handleRead = (
+          directory,
+          initialAdd,
+          throttler = this.throttle('readdir', directory, 1000)
+        ) => {
+          directory = sysPath.join(directory, ''); // Normalize the directory name on Windows
+          if (!throttler) return;
+          if (this.closed) return;
+          const previous = this.getWatchedDir(path);
+          const current = new Set();
+          // eslint-disable-next-line
+          return new Promise(async (resolve) => {
+            try {
+              const files = await this.readdir(directory);
+              const all = files.map(async (dirent) => {
+                try {
+                  const basename = dirent.name;
+                  const fullPath = sysPath.resolve(sysPath.join(directory, basename));
+                  let stats: Stats | undefined;
+                  if (this.closed) return;
+                  // TODO: this is what ignoreDir && ignorePath did. Why don't we check dir && symlink perms?
+                  if (this.isIgnored(entryPath(fullPath))) return;
+                  if (
+                    !dirent.isDirectory() &&
+                    !dirent.isSymbolicLink() &&
+                    !this.options.ignorePermissionErrors
+                  ) {
+                    stats = await this.lstat(fullPath);
+                    if (!stats) return;
+                    if (!hasReadPermissions(stats)) return;
+                  }
+                  if (this.closed) return;
+                  const item = sysPath.relative(sysPath.resolve(directory), fullPath);
+                  const path = sysPath.join(directory, item); // looks like absolute path?
+                  current.add(item);
+                  if (dirent.isSymbolicLink()) {
+                    if (this.closed) return;
+                    if (!follow) {
+                      const dir = this.getWatchedDir(directory);
+                      // watch symlink directly (don't follow) and detect changes
+                      this.readyCount++;
+                      let linkPath;
+                      try {
+                        linkPath = await fsrealpath(path);
+                      } catch (e) {
+                        this.emitReady();
+                        return;
+                      }
+                      if (this.closed) return;
+                      if (dir.has(item)) {
+                        if (this.symlinkPaths.get(fullPath) !== linkPath) {
+                          this.symlinkPaths.set(fullPath, linkPath);
+                          this._emit('change', path, stats);
+                        }
+                      } else {
+                        dir.add(item);
+                        this.symlinkPaths.set(fullPath, linkPath);
+                        this._emit('add', path, stats);
+                      }
+                      this.emitReady();
+                      return;
+                    }
+                    // don't follow the same symlink more than once
+                    if (this.symlinkPaths.has(fullPath)) return;
+                    this.symlinkPaths.set(fullPath, true);
+                  }
+                  if (this.closed) return;
+                  // Files which are present in current directory snapshot
+                  // but absent from previous one, are added to watch list and
+                  // emit `add` event.
+                  if (item === target || (!target && !previous.has(item))) {
+                    this.readyCount++;
+                    this.addToNodeFs(
+                      // ensure relativeness of path is preserved in case of watcher reuse
+                      sysPath.join(dir, sysPath.relative(dir, path)),
+                      initialAdd,
+                      watchPath,
+                      depth + 1
+                    ); // wh re-used only here
+                  }
+                } catch (err) {
+                  if (!NORMAL_FLOW_ERRORS.has(err.code)) this.handleError(err);
+                }
+              });
+              await Promise.all(all);
+            } catch (err) {
+              if (!NORMAL_FLOW_ERRORS.has(err.code)) {
+                this.handleError(err);
+                return; // promise never resolves?
+              }
+            } finally {
+              resolve(undefined);
+            }
+            // End, only if everything is ok? will create promise which will never resolve!
+            const wasThrottled = throttler ? (throttler as any).clear() : false;
+            // Files which are absent in current directory snapshot,
+            // but present in previous one, emit `remove` event
+            // and are removed from @watched[directory].
+            for (const item of previous) {
+              if (item === directory || current.has(item)) continue;
+              this.remove(directory, item);
+            }
+            // one more time for any missed in case changes came in extremely quickly
+            if (wasThrottled) handleRead(directory, false, throttler);
+          });
+        };
+        // ensure dir is tracked (harmless if redundant)
+        parentDir.add(sysPath.basename(dir));
+        this.getWatchedDir(dir);
+        const maxDepth = this.options.depth;
+        if ((maxDepth == null || depth <= maxDepth) && !this.symlinkPaths.has(realpath)) {
+          if (!target) {
+            // Initial read (before watch)
+            await handleRead(dir, initialAdd);
+            if (this.closed) return;
+          }
+          this.addWatcher(closerPath, dir, (dirPath, stats) => {
+            if (stats && stats.mtimeMs === 0) return; // if current directory is removed, do nothing
+            handleRead(dirPath, false);
+          });
+        }
+      };
+      if (stats.isDirectory()) {
+        const targetPath = follow ? await fsrealpath(path) : path;
+        if (this.closed) return;
+        await _handleDir(path, path, target, targetPath);
+        if (this.closed) return;
+        // preserve this symlink's target path
+        const absPath = sysPath.resolve(path);
+        if (absPath !== targetPath && targetPath !== undefined)
+          this.symlinkPaths.set(absPath, targetPath);
+      } else if (stats.isSymbolicLink()) {
+        // Symlinks doesn't emit any event, only parent directory does
+        const targetPath = follow ? await fsrealpath(path) : path;
+        if (this.closed) return;
+        const parent = sysPath.dirname(path);
+        this.getWatchedDir(parent).add(path);
+        this._emit('add', path, stats);
+        await _handleDir(path, parent, path, targetPath);
+        if (this.closed) return;
+        // preserve this symlink's target path
+        if (targetPath !== undefined) this.symlinkPaths.set(sysPath.resolve(path), targetPath);
+      } else {
+        const handleFile = () => {
+          if (this.closed) return;
+          const dirname = sysPath.dirname(path);
+          const basename = sysPath.basename(path);
+          const parent = this.getWatchedDir(dirname);
+          // stats is always present
+          let prevStats: Stats = stats;
+          // if the file is already being watched, do nothing
+          if (parent.has(basename)) return;
+          const file = path;
+          const listener = async (path: Path, newStats: Stats) => {
+            if (!this.throttle('watch', file, 5)) return;
+            if (!newStats || newStats.mtimeMs === 0) {
+              try {
+                const newStats = await this.stat(file);
+                if (this.closed) return;
+                // This is broken: we cannot check atime at all, it can be empty (noatime), it can be slowly updated (relatime),
+                // modification can be done without read (no atime changed).
+                // Correct way:
+                // oldmtime !== newmtime -> change
+                // oldsize !== size -> change: this way we can catch change, even when mtime is identical
+                // Check that `change` event was not fired because of changed only accessTime.
+                const at = newStats.atimeMs;
+                const mt = newStats.mtimeMs;
+                if (!at || at <= mt || mt !== prevStats.mtimeMs)
+                  this._emit('change', file, newStats);
+                // When inode is changed, we need to re-add file with same path
+                if ((isMacos || isLinux) && prevStats.ino !== newStats.ino) {
+                  this.closeWatcher(path);
+                  this.addWatcher(path, file, listener); // TODO: read file? looks ugly
+                }
+                prevStats = newStats;
+              } catch (error) {
+                // Fix issues where mtime is null but file is still present
+                this.remove(dirname, basename);
+              }
+              // Add is about to be emitted if file not already tracked in parent
+            } else if (parent.has(basename)) {
+              // Check that change event was not fired because of changed only accessTime.
+              const at = newStats.atimeMs;
+              const mt = newStats.mtimeMs;
+              if (!at || at <= mt || mt !== prevStats.mtimeMs) this._emit('change', file, newStats);
+              prevStats = newStats;
+            }
+          };
+          // Kick off the watcher
+          this.addWatcher(path, file, listener);
+          // Emit an add event if we're supposed to
+          if (!(initialAdd && this.options.ignoreInitial) && !this.isIgnored(file)) {
+            if (!this.throttle('add', file, 0)) return;
+            this._emit('add', file, stats);
+          }
+        };
+        handleFile();
+      }
+
+      this.emitReady();
+      return false;
+    } catch (error) {
+      this.emitReady();
+      return path;
+    }
+  }
+
+  private emitWithAll(event: EventName, args: EmitArgs) {
+    this.emit(...args);
+    if (event !== 'error') this.emit('all', ...args);
+  }
+  // Common helpers
+  // --------------
+  /**
+   * Normalize and emit events.
+   * Calling _emit DOES NOT MEAN emit() would be called!
+   * @param {EventName} event Type of event
+   * @param {Path} path File or directory path
+   * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
+   */
+  private async _emit(event: EventName, path: Path, stats?: Stats) {
+    if (this.closed) return;
+    const opts = this.options;
+    if (isWindows) path = sysPath.normalize(path);
+    if (opts.cwd) path = sysPath.relative(opts.cwd, path);
+    const args: EmitArgs = [event, path];
+    if (stats !== undefined) args.push(stats);
+    const awf = opts.awaitWriteFinish;
+    let pw;
+    if (awf && (pw = this.pendingWrites.get(path))) {
+      pw.lastChange = Date.now();
+      return this;
+    }
+    if (opts.atomic) {
+      if (event === 'unlink') {
+        this.pendingUnlinks.set(path, args);
+        setTimeout(
+          () => {
+            this.pendingUnlinks.forEach((entry: EmitArgs, path: Path) => {
+              this.emit(...entry);
+              this.emit('all', ...entry);
+              this.pendingUnlinks.delete(path);
+            });
+          },
+          typeof opts.atomic === 'number' ? opts.atomic : 100 // TODO: defaults should be in constructor
+        );
+        return this;
+      }
+      if (event === 'add' && this.pendingUnlinks.has(path)) {
+        event = args[0] = 'change';
+        this.pendingUnlinks.delete(path);
+      }
+    }
+    const fullPath = opts.cwd ? sysPath.join(opts.cwd, path) : path;
+    if (
+      opts.alwaysStat &&
+      stats === undefined &&
+      (event === 'add' || event === 'addDir' || event === 'change')
+    ) {
+      let stats;
+      try {
+        stats = await this.stat(fullPath);
+      } catch (err) {
+        // do nothing
+      }
+      // Suppress event when fs_stat fails, to avoid sending undefined 'stat'
+      if (!stats || this.closed) return;
+      args.push(stats);
+    }
+    if (
+      awf &&
+      typeof awf === 'object' &&
+      (event === 'add' || event === 'change') &&
+      this.readyEmitted
+    ) {
+      const threshold = awf.stabilityThreshold;
+      if (!this.pendingWrites.has(path)) {
+        let timeoutHandler;
+        this.pendingWrites.set(path, {
+          lastChange: Date.now(),
+          cancelWait: () => {
+            this.pendingWrites.delete(path);
+            clearTimeout(timeoutHandler);
+            return event;
+          },
+        });
+        // TODO: cleanup
+        const awaitWriteFinish = async (prevStat?: Stats) => {
+          try {
+            const curStat = await this.stat(fullPath);
+            if (!this.pendingWrites.has(path)) return;
+            const now = Date.now();
+            if (prevStat && curStat.size !== prevStat.size)
+              this.pendingWrites.get(path).lastChange = now;
+            const pw = this.pendingWrites.get(path);
+            const df = now - pw.lastChange;
+            if (df >= threshold) {
+              this.pendingWrites.delete(path);
+              this.emitWithAll(event, [event, path, curStat]);
+            } else timeoutHandler = setTimeout(awaitWriteFinish, awf.pollInterval, curStat);
+          } catch (err) {
+            if (err && err.code !== 'ENOENT') this.emitWithAll(event, ['error', err as any]);
+          }
+        };
+        timeoutHandler = setTimeout(awaitWriteFinish, awf.pollInterval);
+      }
+      return this;
+    }
+    if (event === 'change' && !this.throttle('change', path, 50)) return this;
+    this.emitWithAll(event, args);
+    return this;
+  }
+  /**
+   * Common handler for errors
+   */
+  private handleError(error: Error & { code?: string }, path?: Path) {
+    const code = error && error.code;
+    if (
+      error &&
+      code !== 'ENOENT' &&
+      code !== 'ENOTDIR' &&
+      (!this.options.ignorePermissionErrors || (code !== 'EPERM' && code !== 'EACCES'))
+    ) {
+      // TODO: this problem still exists in node v18 + windows 11
+      // supressing error doesn't actually fix it, since watcher is unusable after that
+      // Worth fixing later
+      // Workaround for https://github.com/joyent/node/issues/4337
+      if (isWindows && error.code === 'EPERM') {
+        (async () => {
+          if (await this.canOpen(path)) this.emit('error', error);
+        })();
+        return;
+      }
+      this.emit('error', error);
+    }
+  }
+  /**
+   * Determines whether user has asked to ignore this path.
+   */
+  private isIgnored(path: Path, stats?: Stats) {
+    // Temporary files for editors with atomic write. This probably should be handled separately.
+    const DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
+    if (this.options.atomic && DOT_RE.test(path)) return true;
+    if (!this.userIgnored) {
+      const list: Matcher[] = [...this.ignoredPaths, ...(this.options.ignored || [])].map((path) =>
+        typeof path === 'string' ? this.normalizePath(path) : path
+      );
+      // Early cache for matchers.
+      const patterns = list.map((matcher) => {
+        if (typeof matcher === 'function') return matcher;
+        if (typeof matcher === 'string') return (string) => matcher === string;
+        if (matcher instanceof RegExp) return (string) => matcher.test(string);
+        // TODO: remove / refactor
+        if (typeof matcher === 'object' && matcher !== null) {
+          return (string) => {
+            if (matcher.path === string) return true;
+            if (matcher.recursive) {
+              const relative = sysPath.relative(matcher.path, string);
+              if (!relative) return false;
+              return !relative.startsWith('..') && !sysPath.isAbsolute(relative);
+            }
+            return false;
+          };
+        }
+        return () => false;
+      });
+      this.userIgnored = (path: string, stats?: Stats): boolean => {
+        path = this.normalizePath(path);
+        for (const pattern of patterns) if (pattern(path, stats)) return true;
+        return false;
+      };
+    }
+    return this.userIgnored(path, stats);
+  }
+  /**
+   * Handles emitting unlink events for
+   * files and directories, and via recursion, for
+   * files and directories within directories that are unlinked
+   * @param directory within which the following item is located
+   * @param item      base path of item/directory
+   */
+  private remove(directory: string, item: string, isDirectory?: boolean) {
+    // When a directory is deleted, get its paths for recursive deletion
+    // and cleaning of watched object.
+    // When not a directory, nestedDirectoryChildren will be empty.
+    const path = sysPath.join(directory, item);
+    const fullPath = sysPath.resolve(path);
+    isDirectory =
+      isDirectory != null ? isDirectory : this.watched.has(path) || this.watched.has(fullPath);
+    // prevent duplicate handling in case of arriving here nearly simultaneously
+    // via multiple paths (such as _handleFile and _handleDir)
+    if (!this.throttle('remove', path, 100)) return;
+    // if the only watched file is removed, watch for its return
+    if (!isDirectory && this.watched.size === 1) this.add(directory, item, true);
+    // This will create a new entry in the watched object in either case
+    // so we got to do the directory check beforehand
+    const wp = this.getWatchedDir(path);
+    // Recursively remove children directories / files.
+    wp.forEach((nested) => this.remove(path, nested));
+    // Check if item was on the watched list and remove it
+    const parent = this.getWatchedDir(directory);
+    const wasTracked = parent.has(item);
+    parent.delete(item);
+    this.canOpenDir(directory);
+    // Fixes issue #1042 -> Relative paths were detected and added as symlinks
+    // (https://github.com/paulmillr/chokidar/blob/e1753ddbc9571bdc33b4a4af172d52cb6e611c10/lib/nodefs-handler.js#L612),
+    // but never removed from the map in case the path was deleted.
+    // This leads to an incorrect state if the path was recreated:
+    // https://github.com/paulmillr/chokidar/blob/e1753ddbc9571bdc33b4a4af172d52cb6e611c10/lib/nodefs-handler.js#L553
+    if (this.symlinkPaths.has(fullPath)) this.symlinkPaths.delete(fullPath);
+    // If we wait for this file to be fully written, cancel the wait.
+    let relPath = path;
+    if (this.options.cwd) relPath = sysPath.relative(this.options.cwd, path);
+    if (this.options.awaitWriteFinish && this.pendingWrites.has(relPath)) {
+      const event = this.pendingWrites.get(relPath).cancelWait();
+      if (event === 'add') return;
+    }
+    // The Entry will either be a directory that just got removed
+    // or a bogus entry to a file, in either case we have to remove it
+    this.watched.delete(path);
+    this.watched.delete(fullPath);
+    const eventName: EventName = isDirectory ? 'unlinkDir' : 'unlink';
+    if (wasTracked && !this.isIgnored(path)) this._emit(eventName, path);
+    // Avoid conflicts if we later create another file with the same name
+    this.closeWatcher(path);
+  }
+
+  // Public API
+  /**
+   * Adds paths to be watched on an existing FSWatcher instance
+   * @param {Path|Array<Path>} paths_
+   * @param {String=} _origAdd private; for handling non-existent paths to be watched
+   * @param {Boolean=} _internal private; indicates a non-user add
+   * @returns {FSWatcher} for chaining
+   */
+  add(paths_: Path | Path[], _origAdd?: string, _internal?: boolean) {
+    this.closed = false;
+    const paths = this.normalizePaths(paths_);
+    paths.forEach((matcher) => {
+      this.ignoredPaths.delete(matcher);
+      // now find any matcher objects with the matcher as path
+      if (typeof matcher === 'string') {
+        for (const ignored of this.ignoredPaths) {
+          // TODO (43081j): make this more efficient.
+          // probably just make a `this._ignoredDirectories` or some
+          // such thing.
+          if (isMatcherObject(ignored) && ignored.path === matcher)
+            this.ignoredPaths.delete(ignored);
+        }
+      }
+    });
+    this.userIgnored = undefined;
+    if (!this.readyCount) this.readyCount = 0;
+    this.readyCount += paths.length;
+    Promise.all(
+      paths.map(async (path) => {
+        const res = await this.addToNodeFs(path, !_internal, undefined, 0, _origAdd);
+        if (this.closed) return;
+        if (res) {
+          this.emitReady();
+          this.add(sysPath.dirname(res), sysPath.basename(_origAdd || res));
+        }
+        return res;
+      })
+    );
+    return this;
+  }
+  /**
+   * Close watchers or start ignoring events from specified paths.
+   * @param {Path|Array<Path>} paths - string or array of strings, file/directory paths
+   * @returns {FSWatcher} for chaining
+   */
+  unwatch(paths: Path | Path[]) {
+    if (this.closed) return this;
+    paths = flatten(arrify(paths));
+    //paths = this.normalizePaths(paths);
+    for (let path of paths) {
+      const { cwd } = this.options;
+      // If path relative and
+      if (!sysPath.isAbsolute(path) && !this.closers.has(path)) {
+        if (cwd) path = sysPath.join(cwd, path);
+        path = sysPath.resolve(path);
+      }
+      this.closeWatcher(path);
+      if (isMatcherObject(path)) {
+        // return early if we already have a deeply equal matcher object
+        for (const ignored of this.ignoredPaths) {
+          if (
+            isMatcherObject(ignored) &&
+            ignored.path === path.path &&
+            ignored.recursive === path.recursive
+          ) {
+            continue;
+          }
+        }
+      }
+      this.ignoredPaths.add(path);
+      this.userIgnored = undefined; // reset the cached userIgnored fn
+    }
+    return this;
+  }
+  /**
+   * Expose list of watched paths
+   * @returns {Record<string, string[]>}
+   */
+  getWatched() {
+    const watchList = {};
+    this.watched.forEach((entry, dir) => {
+      const key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir;
+      watchList[key || '.'] = Array.from(entry).sort();
+    });
+    return watchList;
+  }
+  /**
+   * Close watchers and remove all listeners from watched paths.
+   */
+  close() {
+    if (this.closed) return this.closePromise;
+    this.closed = true;
+    // Memory management.
+    this.removeAllListeners();
+    const closers = [];
+    this.closers.forEach((closerList) =>
+      closerList.forEach((closer) => {
+        const promise = closer();
+        if (promise instanceof Promise) closers.push(promise);
+      })
+    );
+    this.userIgnored = undefined;
+    // this allows to re-start?
+    this.readyCount = 0;
+    this.readyEmitted = false;
+    this.watched.forEach((dirent) => dirent.clear());
+    ['closers', 'watched', 'symlinkPaths', 'throttled'].forEach((key) => {
+      this[key].clear();
+    });
+    this.metrics = {};
+    this.closePromise = closers.length
+      ? Promise.all(closers).then(() => undefined)
+      : Promise.resolve();
+    return this.closePromise;
+  }
+}
+
+// Public API
+
+/**
+ * Instantiates watcher with paths to be tracked.
+ * @param paths file/directory paths and/or globs
+ * @param options chokidar opts
+ * @returns an instance of FSWatcher for chaining.
+ */
+export const watch = (paths: Path | Path[], options: ChokidarOptions) => {
+  const watcher = new FSWatcher(options);
+  watcher.add(paths);
+  return watcher;
+};
+
+export default { watch, FSWatcher };
diff --git a/test-v4.mjs b/test-v4.mjs
new file mode 100644
index 00000000..a87857bc
--- /dev/null
+++ b/test-v4.mjs
@@ -0,0 +1,2138 @@
+import fs from 'node:fs';
+import sysPath from 'node:path';
+import { describe, it, before, after, beforeEach, afterEach } from 'node:test';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { promisify } from 'node:util';
+import childProcess from 'node:child_process';
+import chai from 'chai';
+import { rimraf } from 'rimraf';
+import sinon from 'sinon';
+import sinonChai from 'sinon-chai';
+import upath from 'upath';
+
+import chokidar from './lib/v4.js';
+import { isWindows, isMacos, isIBMi, EV } from './lib/v4.js';
+
+import { URL } from 'url'; // in Browser, the URL in native accessible on window
+
+const __filename = fileURLToPath(new URL('', import.meta.url));
+// Will contain trailing slash
+const __dirname = fileURLToPath(new URL('.', import.meta.url));
+
+const { expect } = chai;
+chai.use(sinonChai);
+chai.should();
+
+const exec = promisify(childProcess.exec);
+const write = promisify(fs.writeFile);
+const fs_symlink = promisify(fs.symlink);
+const fs_rename = promisify(fs.rename);
+const fs_mkdir = promisify(fs.mkdir);
+const fs_rmdir = promisify(fs.rmdir);
+const fs_unlink = promisify(fs.unlink);
+
+const FIXTURES_PATH_REL = 'test-fixtures';
+const FIXTURES_PATH = sysPath.join(__dirname, FIXTURES_PATH_REL);
+const allWatchers = [];
+const PERM_ARR = 0o755; // rwe, r+e, r+e
+const TEST_TIMEOUT = 8000;
+let subdirId = 0;
+let options;
+let currentDir;
+let slowerDelay;
+
+// spyOnReady
+const aspy = (watcher, eventName, spy = null, noStat = false) => {
+  if (typeof eventName !== 'string') {
+    throw new TypeError('aspy: eventName must be a String');
+  }
+  if (spy == null) spy = sinon.spy();
+  return new Promise((resolve, reject) => {
+    const handler = noStat
+      ? eventName === EV.ALL
+        ? (event, path) => spy(event, path)
+        : (path) => spy(path)
+      : spy;
+    const timeout = setTimeout(() => {
+      reject(new Error('timeout'));
+    }, TEST_TIMEOUT);
+    watcher.on(EV.ERROR, (...args) => {
+      clearTimeout(timeout);
+      reject(...args);
+    });
+    watcher.on(EV.READY, () => {
+      clearTimeout(timeout);
+      resolve(spy);
+    });
+    watcher.on(eventName, handler);
+  });
+};
+
+const waitForWatcher = (watcher) => {
+  return new Promise((resolve, reject) => {
+    const timeout = setTimeout(() => {
+      reject(new Error('timeout'));
+    }, TEST_TIMEOUT);
+    watcher.on(EV.ERROR, (...args) => {
+      clearTimeout(timeout);
+      reject(...args);
+    });
+    watcher.on(EV.READY, (...args) => {
+      clearTimeout(timeout);
+      resolve(...args);
+    });
+  });
+};
+
+const delay = async (time) => {
+  return new Promise((resolve) => {
+    const timer = time || slowerDelay || 20;
+    setTimeout(resolve, timer);
+  });
+};
+
+const getFixturePath = (subPath) => {
+  const subd = (subdirId && subdirId.toString()) || '';
+  return sysPath.join(FIXTURES_PATH, subd, subPath);
+};
+const getGlobPath = (subPath) => {
+  const subd = (subdirId && subdirId.toString()) || '';
+  return upath.join(FIXTURES_PATH, subd, subPath);
+};
+currentDir = getFixturePath('');
+
+const chokidar_watch = (path = currentDir, opts = options) => {
+  const wt = chokidar.watch(path, opts);
+  allWatchers.push(wt);
+  return wt;
+};
+
+const waitFor = async (spies) => {
+  if (spies.length === 0) throw new TypeError('SPies zero');
+  return new Promise((resolve, reject) => {
+    const timeout = setTimeout(() => {
+      reject(new Error('timeout'));
+    }, TEST_TIMEOUT);
+    const isSpyReady = (spy) => {
+      if (Array.isArray(spy)) {
+        return spy[0].callCount >= spy[1];
+      }
+      return spy.callCount >= 1;
+    };
+    const checkSpiesReady = () => {
+      if (spies.every(isSpyReady)) {
+        clearTimeout(timeout);
+        resolve();
+      } else {
+        setTimeout(checkSpiesReady, 20);
+      }
+    };
+    checkSpiesReady();
+  });
+};
+
+const waitForEvents = (watcher, count) => {
+  return new Promise((resolve, reject) => {
+    const timeout = setTimeout(() => {
+      reject(new Error('timeout'));
+    }, TEST_TIMEOUT);
+    const events = [];
+    const handler = (event, path) => {
+      events.push(`[ALL] ${event}: ${path}`);
+
+      if (events.length === count) {
+        watcher.off('all', handler);
+        clearTimeout(timeout);
+        resolve(events);
+      }
+    };
+
+    watcher.on('all', handler);
+  });
+};
+
+const dateNow = () => Date.now().toString();
+
+const runTests = (baseopts) => {
+  let macosFswatch;
+  let win32Polling;
+
+  baseopts.persistent = true;
+
+  before(() => {
+    // flags for bypassing special-case test failures on CI
+    macosFswatch = isMacos && !baseopts.usePolling;
+    win32Polling = isWindows && baseopts.usePolling;
+    slowerDelay = macosFswatch ? 100 : undefined;
+  });
+
+  beforeEach(function clean() {
+    options = {};
+    Object.keys(baseopts).forEach((key) => {
+      options[key] = baseopts[key];
+    });
+  });
+
+  describe('watch a directory', () => {
+    let readySpy, rawSpy, watcher, watcher2;
+    beforeEach(async () => {
+      options.ignoreInitial = true;
+      options.alwaysStat = true;
+      readySpy = sinon.spy(function readySpy() {});
+      rawSpy = sinon.spy(function rawSpy() {});
+      watcher = chokidar_watch().on(EV.READY, readySpy).on(EV.RAW, rawSpy);
+      await waitForWatcher(watcher);
+    });
+    afterEach(async () => {
+      await waitFor([readySpy]);
+      await watcher.close();
+      readySpy.should.have.been.calledOnce;
+      readySpy = undefined;
+      rawSpy = undefined;
+    });
+    it('should produce an instance of chokidar.FSWatcher', () => {
+      watcher.should.be.an.instanceof(chokidar.FSWatcher);
+    });
+    it('should expose public API methods', () => {
+      watcher.on.should.be.a('function');
+      watcher.emit.should.be.a('function');
+      watcher.add.should.be.a('function');
+      watcher.close.should.be.a('function');
+      watcher.getWatched.should.be.a('function');
+    });
+    it('should emit `add` event when file was added', async () => {
+      const testPath = getFixturePath('add.txt');
+      const spy = sinon.spy(function addSpy() {});
+      watcher.on(EV.ADD, spy);
+      await delay();
+      await write(testPath, dateNow());
+      await waitFor([spy]);
+      spy.should.have.been.calledOnce;
+      spy.should.have.been.calledWith(testPath);
+      expect(spy.args[0][1]).to.be.ok; // stats
+      rawSpy.should.have.been.called;
+    });
+    it('should emit nine `add` events when nine files were added in one directory', async () => {
+      const paths = [];
+      for (let i = 1; i <= 9; i++) {
+        paths.push(getFixturePath(`add${i}.txt`));
+      }
+
+      const spy = sinon.spy();
+      watcher.on(EV.ADD, (path) => {
+        spy(path);
+      });
+
+      write(paths[0], dateNow());
+      write(paths[1], dateNow());
+      write(paths[2], dateNow());
+      write(paths[3], dateNow());
+      write(paths[4], dateNow());
+      await delay(100);
+
+      write(paths[5], dateNow());
+      write(paths[6], dateNow());
+
+      await delay(150);
+      write(paths[7], dateNow());
+      write(paths[8], dateNow());
+
+      await waitFor([[spy, 4]]);
+
+      await delay(1000);
+      await waitFor([[spy, 9]]);
+      paths.forEach((path) => {
+        spy.should.have.been.calledWith(path);
+      });
+    });
+    it('should emit thirtythree `add` events when thirtythree files were added in nine directories', async () => {
+      await watcher.close();
+
+      const test1Path = getFixturePath('add1.txt');
+      const testb1Path = getFixturePath('b/add1.txt');
+      const testc1Path = getFixturePath('c/add1.txt');
+      const testd1Path = getFixturePath('d/add1.txt');
+      const teste1Path = getFixturePath('e/add1.txt');
+      const testf1Path = getFixturePath('f/add1.txt');
+      const testg1Path = getFixturePath('g/add1.txt');
+      const testh1Path = getFixturePath('h/add1.txt');
+      const testi1Path = getFixturePath('i/add1.txt');
+      const test2Path = getFixturePath('add2.txt');
+      const testb2Path = getFixturePath('b/add2.txt');
+      const testc2Path = getFixturePath('c/add2.txt');
+      const test3Path = getFixturePath('add3.txt');
+      const testb3Path = getFixturePath('b/add3.txt');
+      const testc3Path = getFixturePath('c/add3.txt');
+      const test4Path = getFixturePath('add4.txt');
+      const testb4Path = getFixturePath('b/add4.txt');
+      const testc4Path = getFixturePath('c/add4.txt');
+      const test5Path = getFixturePath('add5.txt');
+      const testb5Path = getFixturePath('b/add5.txt');
+      const testc5Path = getFixturePath('c/add5.txt');
+      const test6Path = getFixturePath('add6.txt');
+      const testb6Path = getFixturePath('b/add6.txt');
+      const testc6Path = getFixturePath('c/add6.txt');
+      const test7Path = getFixturePath('add7.txt');
+      const testb7Path = getFixturePath('b/add7.txt');
+      const testc7Path = getFixturePath('c/add7.txt');
+      const test8Path = getFixturePath('add8.txt');
+      const testb8Path = getFixturePath('b/add8.txt');
+      const testc8Path = getFixturePath('c/add8.txt');
+      const test9Path = getFixturePath('add9.txt');
+      const testb9Path = getFixturePath('b/add9.txt');
+      const testc9Path = getFixturePath('c/add9.txt');
+      fs.mkdirSync(getFixturePath('b'), PERM_ARR);
+      fs.mkdirSync(getFixturePath('c'), PERM_ARR);
+      fs.mkdirSync(getFixturePath('d'), PERM_ARR);
+      fs.mkdirSync(getFixturePath('e'), PERM_ARR);
+      fs.mkdirSync(getFixturePath('f'), PERM_ARR);
+      fs.mkdirSync(getFixturePath('g'), PERM_ARR);
+      fs.mkdirSync(getFixturePath('h'), PERM_ARR);
+      fs.mkdirSync(getFixturePath('i'), PERM_ARR);
+
+      await delay();
+
+      readySpy.resetHistory();
+      watcher2 = chokidar_watch().on(EV.READY, readySpy).on(EV.RAW, rawSpy);
+      const spy = await aspy(watcher2, EV.ADD, null, true);
+
+      const filesToWrite = [
+        test1Path,
+        test2Path,
+        test3Path,
+        test4Path,
+        test5Path,
+        test6Path,
+        test7Path,
+        test8Path,
+        test9Path,
+        testb1Path,
+        testb2Path,
+        testb3Path,
+        testb4Path,
+        testb5Path,
+        testb6Path,
+        testb7Path,
+        testb8Path,
+        testb9Path,
+        testc1Path,
+        testc2Path,
+        testc3Path,
+        testc4Path,
+        testc5Path,
+        testc6Path,
+        testc7Path,
+        testc8Path,
+        testc9Path,
+        testd1Path,
+        teste1Path,
+        testf1Path,
+        testg1Path,
+        testh1Path,
+        testi1Path,
+      ];
+
+      let currentCallCount = 0;
+
+      for (const fileToWrite of filesToWrite) {
+        await write(fileToWrite, dateNow());
+        await waitFor([[spy, ++currentCallCount]]);
+      }
+
+      spy.should.have.been.calledWith(test1Path);
+      spy.should.have.been.calledWith(test2Path);
+      spy.should.have.been.calledWith(test3Path);
+      spy.should.have.been.calledWith(test4Path);
+      spy.should.have.been.calledWith(test5Path);
+      spy.should.have.been.calledWith(test6Path);
+      spy.should.have.been.calledWith(test7Path);
+      spy.should.have.been.calledWith(test8Path);
+      spy.should.have.been.calledWith(test9Path);
+      spy.should.have.been.calledWith(testb1Path);
+      spy.should.have.been.calledWith(testb2Path);
+      spy.should.have.been.calledWith(testb3Path);
+      spy.should.have.been.calledWith(testb4Path);
+      spy.should.have.been.calledWith(testb5Path);
+      spy.should.have.been.calledWith(testb6Path);
+      spy.should.have.been.calledWith(testb7Path);
+      spy.should.have.been.calledWith(testb8Path);
+      spy.should.have.been.calledWith(testb9Path);
+      spy.should.have.been.calledWith(testc1Path);
+      spy.should.have.been.calledWith(testc2Path);
+      spy.should.have.been.calledWith(testc3Path);
+      spy.should.have.been.calledWith(testc4Path);
+      spy.should.have.been.calledWith(testc5Path);
+      spy.should.have.been.calledWith(testc6Path);
+      spy.should.have.been.calledWith(testc7Path);
+      spy.should.have.been.calledWith(testc8Path);
+      spy.should.have.been.calledWith(testc9Path);
+      spy.should.have.been.calledWith(testd1Path);
+      spy.should.have.been.calledWith(teste1Path);
+      spy.should.have.been.calledWith(testf1Path);
+      spy.should.have.been.calledWith(testg1Path);
+      spy.should.have.been.calledWith(testh1Path);
+      spy.should.have.been.calledWith(testi1Path);
+    });
+    it('should emit `addDir` event when directory was added', async () => {
+      const testDir = getFixturePath('subdir');
+      const spy = sinon.spy(function addDirSpy() {});
+      watcher.on(EV.ADD_DIR, spy);
+      spy.should.not.have.been.called;
+      await fs_mkdir(testDir, PERM_ARR);
+      await waitFor([spy]);
+      spy.should.have.been.calledOnce;
+      spy.should.have.been.calledWith(testDir);
+      expect(spy.args[0][1]).to.be.ok; // stats
+      rawSpy.should.have.been.called;
+    });
+    it('should emit `change` event when file was changed', async () => {
+      const testPath = getFixturePath('change.txt');
+      const spy = sinon.spy(function changeSpy() {});
+      watcher.on(EV.CHANGE, spy);
+      spy.should.not.have.been.called;
+      await write(testPath, dateNow());
+      await waitFor([spy]);
+      spy.should.have.been.calledWith(testPath);
+      expect(spy.args[0][1]).to.be.ok; // stats
+      rawSpy.should.have.been.called;
+      spy.should.have.been.calledOnce;
+    });
+    it('should emit `unlink` event when file was removed', async () => {
+      const testPath = getFixturePath('unlink.txt');
+      const spy = sinon.spy(function unlinkSpy() {});
+      watcher.on(EV.UNLINK, spy);
+      spy.should.not.have.been.called;
+      await fs_unlink(testPath);
+      await waitFor([spy]);
+      spy.should.have.been.calledWith(testPath);
+      expect(spy.args[0][1]).to.not.be.ok; // no stats
+      rawSpy.should.have.been.called;
+      spy.should.have.been.calledOnce;
+    });
+    it('should emit `unlinkDir` event when a directory was removed', async () => {
+      const testDir = getFixturePath('subdir');
+      const spy = sinon.spy(function unlinkDirSpy() {});
+
+      await fs_mkdir(testDir, PERM_ARR);
+      await delay(300);
+      watcher.on(EV.UNLINK_DIR, spy);
+
+      await fs_rmdir(testDir);
+      await waitFor([spy]);
+      spy.should.have.been.calledWith(testDir);
+      expect(spy.args[0][1]).to.not.be.ok; // no stats
+      rawSpy.should.have.been.called;
+      spy.should.have.been.calledOnce;
+    });
+    it('should emit two `unlinkDir` event when two nested directories were removed', async () => {
+      const testDir = getFixturePath('subdir');
+      const testDir2 = getFixturePath('subdir/subdir2');
+      const testDir3 = getFixturePath('subdir/subdir2/subdir3');
+      const spy = sinon.spy(function unlinkDirSpy() {});
+
+      await fs_mkdir(testDir, PERM_ARR);
+      await fs_mkdir(testDir2, PERM_ARR);
+      await fs_mkdir(testDir3, PERM_ARR);
+      await delay(300);
+
+      watcher.on(EV.UNLINK_DIR, spy);
+
+      await rimraf(testDir2);
+      await waitFor([[spy, 2]]);
+
+      spy.should.have.been.calledWith(testDir2);
+      spy.should.have.been.calledWith(testDir3);
+      expect(spy.args[0][1]).to.not.be.ok; // no stats
+      rawSpy.should.have.been.called;
+      spy.should.have.been.calledTwice;
+    });
+    it('should emit `unlink` and `add` events when a file is renamed', async () => {
+      const unlinkSpy = sinon.spy(function unlink() {});
+      const addSpy = sinon.spy(function add() {});
+      const testPath = getFixturePath('change.txt');
+      const newPath = getFixturePath('moved.txt');
+      watcher.on(EV.UNLINK, unlinkSpy).on(EV.ADD, addSpy);
+      unlinkSpy.should.not.have.been.called;
+      addSpy.should.not.have.been.called;
+
+      await delay();
+      await fs_rename(testPath, newPath);
+      await waitFor([unlinkSpy, addSpy]);
+      unlinkSpy.should.have.been.calledWith(testPath);
+      expect(unlinkSpy.args[0][1]).to.not.be.ok; // no stats
+      addSpy.should.have.been.calledOnce;
+      addSpy.should.have.been.calledWith(newPath);
+      expect(addSpy.args[0][1]).to.be.ok; // stats
+      rawSpy.should.have.been.called;
+      if (!macosFswatch) unlinkSpy.should.have.been.calledOnce;
+    });
+    it('should emit `add`, not `change`, when previously deleted file is re-added', async () => {
+      const unlinkSpy = sinon.spy(function unlink() {});
+      const addSpy = sinon.spy(function add() {});
+      const changeSpy = sinon.spy(function change() {});
+      const testPath = getFixturePath('add.txt');
+      watcher.on(EV.UNLINK, unlinkSpy).on(EV.ADD, addSpy).on(EV.CHANGE, changeSpy);
+      await write(testPath, 'hello');
+      await waitFor([[addSpy.withArgs(testPath), 1]]);
+      unlinkSpy.should.not.have.been.called;
+      changeSpy.should.not.have.been.called;
+      await fs_unlink(testPath);
+      await waitFor([unlinkSpy.withArgs(testPath)]);
+      unlinkSpy.should.have.been.calledWith(testPath);
+
+      await delay(100);
+      await write(testPath, dateNow());
+      await waitFor([[addSpy.withArgs(testPath), 2]]);
+      addSpy.should.have.been.calledWith(testPath);
+      changeSpy.should.not.have.been.called;
+      expect(addSpy.callCount).to.equal(2);
+    });
+    it('should not emit `unlink` for previously moved files', async () => {
+      const unlinkSpy = sinon.spy(function unlink() {});
+      const testPath = getFixturePath('change.txt');
+      const newPath1 = getFixturePath('moved.txt');
+      const newPath2 = getFixturePath('moved-again.txt');
+      watcher.on(EV.UNLINK, unlinkSpy);
+      await fs_rename(testPath, newPath1);
+
+      await delay(300);
+      await fs_rename(newPath1, newPath2);
+      await waitFor([unlinkSpy.withArgs(newPath1)]);
+      unlinkSpy.withArgs(testPath).should.have.been.calledOnce;
+      unlinkSpy.withArgs(newPath1).should.have.been.calledOnce;
+      unlinkSpy.withArgs(newPath2).should.not.have.been.called;
+    });
+    it('should survive ENOENT for missing subdirectories', async () => {
+      const testDir = getFixturePath('notadir');
+      watcher.add(testDir);
+    });
+    it('should notice when a file appears in a new directory', async () => {
+      const testDir = getFixturePath('subdir');
+      const testPath = getFixturePath('subdir/add.txt');
+      const spy = sinon.spy(function addSpy() {});
+      watcher.on(EV.ADD, spy);
+      spy.should.not.have.been.called;
+      await fs_mkdir(testDir, PERM_ARR);
+      await write(testPath, dateNow());
+      await waitFor([spy]);
+      spy.should.have.been.calledOnce;
+      spy.should.have.been.calledWith(testPath);
+      expect(spy.args[0][1]).to.be.ok; // stats
+      rawSpy.should.have.been.called;
+    });
+    it('should watch removed and re-added directories', async () => {
+      const unlinkSpy = sinon.spy(function unlinkSpy() {});
+      const addSpy = sinon.spy(function addSpy() {});
+      const parentPath = getFixturePath('subdir2');
+      const subPath = getFixturePath('subdir2/subsub');
+      watcher.on(EV.UNLINK_DIR, unlinkSpy).on(EV.ADD_DIR, addSpy);
+      await fs_mkdir(parentPath, PERM_ARR);
+
+      await delay(win32Polling ? 900 : 300);
+      await fs_rmdir(parentPath);
+      await waitFor([unlinkSpy.withArgs(parentPath)]);
+      unlinkSpy.should.have.been.calledWith(parentPath);
+      await fs_mkdir(parentPath, PERM_ARR);
+
+      await delay(win32Polling ? 2200 : 1200);
+      await fs_mkdir(subPath, PERM_ARR);
+      await waitFor([[addSpy, 3]]);
+      addSpy.should.have.been.calledWith(parentPath);
+      addSpy.should.have.been.calledWith(subPath);
+    });
+    it('should emit `unlinkDir` and `add` when dir is replaced by file', async () => {
+      options.ignoreInitial = true;
+      const unlinkSpy = sinon.spy(function unlinkSpy() {});
+      const addSpy = sinon.spy(function addSpy() {});
+      const testPath = getFixturePath('dirFile');
+      await fs_mkdir(testPath, PERM_ARR);
+      await delay(300);
+      watcher.on(EV.UNLINK_DIR, unlinkSpy).on(EV.ADD, addSpy);
+
+      await fs_rmdir(testPath);
+      await waitFor([unlinkSpy]);
+
+      await write(testPath, 'file content');
+      await waitFor([addSpy]);
+
+      unlinkSpy.should.have.been.calledWith(testPath);
+      addSpy.should.have.been.calledWith(testPath);
+    });
+    it('should emit `unlink` and `addDir` when file is replaced by dir', async () => {
+      options.ignoreInitial = true;
+      const unlinkSpy = sinon.spy(function unlinkSpy() {});
+      const addSpy = sinon.spy(function addSpy() {});
+      const testPath = getFixturePath('fileDir');
+      await write(testPath, 'file content');
+      watcher.on(EV.UNLINK, unlinkSpy).on(EV.ADD_DIR, addSpy);
+
+      await delay(300);
+      await fs_unlink(testPath);
+      await delay(300);
+      await fs_mkdir(testPath, PERM_ARR);
+
+      await waitFor([addSpy, unlinkSpy]);
+      unlinkSpy.should.have.been.calledWith(testPath);
+      addSpy.should.have.been.calledWith(testPath);
+    });
+  });
+  describe('watch individual files', () => {
+    it('should emit `ready` when three files were added', async () => {
+      const readySpy = sinon.spy(function readySpy() {});
+      const watcher = chokidar_watch().on(EV.READY, readySpy);
+      const path1 = getFixturePath('add1.txt');
+      const path2 = getFixturePath('add2.txt');
+      const path3 = getFixturePath('add3.txt');
+
+      watcher.add(path1);
+      watcher.add(path2);
+      watcher.add(path3);
+
+      await waitForWatcher(watcher);
+      // callCount is 1 on macOS, 4 on Ubuntu
+      readySpy.callCount.should.be.greaterThanOrEqual(1);
+    });
+    it('should detect changes', async () => {
+      const testPath = getFixturePath('change.txt');
+      const watcher = chokidar_watch(testPath, options);
+      const spy = await aspy(watcher, EV.CHANGE);
+      await write(testPath, dateNow());
+      await waitFor([spy]);
+      spy.should.have.always.been.calledWith(testPath);
+    });
+    it('should detect unlinks', async () => {
+      const testPath = getFixturePath('unlink.txt');
+      const watcher = chokidar_watch(testPath, options);
+      const spy = await aspy(watcher, EV.UNLINK);
+
+      await delay();
+      await fs_unlink(testPath);
+      await waitFor([spy]);
+      spy.should.have.been.calledWith(testPath);
+    });
+    it('should detect unlink and re-add', async () => {
+      options.ignoreInitial = true;
+      const unlinkSpy = sinon.spy(function unlinkSpy() {});
+      const addSpy = sinon.spy(function addSpy() {});
+      const testPath = getFixturePath('unlink.txt');
+      const watcher = chokidar_watch([testPath], options)
+        .on(EV.UNLINK, unlinkSpy)
+        .on(EV.ADD, addSpy);
+      await waitForWatcher(watcher);
+
+      await delay();
+      await fs_unlink(testPath);
+      await waitFor([unlinkSpy]);
+      unlinkSpy.should.have.been.calledWith(testPath);
+
+      await delay();
+      await write(testPath, 're-added');
+      await waitFor([addSpy]);
+      addSpy.should.have.been.calledWith(testPath);
+    });
+
+    it('should ignore unwatched siblings', async () => {
+      const testPath = getFixturePath('add.txt');
+      const siblingPath = getFixturePath('change.txt');
+      const watcher = chokidar_watch(testPath, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      await delay();
+      await write(siblingPath, dateNow());
+      await write(testPath, dateNow());
+      await waitFor([spy]);
+      spy.should.have.always.been.calledWith(EV.ADD, testPath);
+    });
+
+    it('should detect safe-edit', async () => {
+      const testPath = getFixturePath('change.txt');
+      const safePath = getFixturePath('tmp.txt');
+      await write(testPath, dateNow());
+      const watcher = chokidar_watch(testPath, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      await delay();
+      await write(safePath, dateNow());
+      await fs_rename(safePath, testPath);
+      await delay(300);
+      await write(safePath, dateNow());
+      await fs_rename(safePath, testPath);
+      await delay(300);
+      await write(safePath, dateNow());
+      await fs_rename(safePath, testPath);
+      await delay(300);
+      await waitFor([spy]);
+      spy.withArgs(EV.CHANGE, testPath).should.have.been.calledThrice;
+    });
+
+    // PR 682 is failing.
+    describe.skip('Skipping gh-682: should detect unlink', () => {
+      it('should detect unlink while watching a non-existent second file in another directory', async () => {
+        const testPath = getFixturePath('unlink.txt');
+        const otherDirPath = getFixturePath('other-dir');
+        const otherPath = getFixturePath('other-dir/other.txt');
+        fs.mkdirSync(otherDirPath, PERM_ARR);
+        const watcher = chokidar_watch([testPath, otherPath], options);
+        // intentionally for this test don't write fs.writeFileSync(otherPath, 'other');
+        const spy = await aspy(watcher, EV.UNLINK);
+
+        await delay();
+        await fs_unlink(testPath);
+        await waitFor([spy]);
+        spy.should.have.been.calledWith(testPath);
+      });
+      it('should detect unlink and re-add while watching a second file', async () => {
+        options.ignoreInitial = true;
+        const unlinkSpy = sinon.spy(function unlinkSpy() {});
+        const addSpy = sinon.spy(function addSpy() {});
+        const testPath = getFixturePath('unlink.txt');
+        const otherPath = getFixturePath('other.txt');
+        fs.writeFileSync(otherPath, 'other');
+        const watcher = chokidar_watch([testPath, otherPath], options)
+          .on(EV.UNLINK, unlinkSpy)
+          .on(EV.ADD, addSpy);
+        await waitForWatcher(watcher);
+
+        await delay();
+        await fs_unlink(testPath);
+        await waitFor([unlinkSpy]);
+
+        await delay();
+        unlinkSpy.should.have.been.calledWith(testPath);
+
+        await delay();
+        write(testPath, 're-added');
+        await waitFor([addSpy]);
+        addSpy.should.have.been.calledWith(testPath);
+      });
+      it('should detect unlink and re-add while watching a non-existent second file in another directory', async () => {
+        options.ignoreInitial = true;
+        const unlinkSpy = sinon.spy(function unlinkSpy() {});
+        const addSpy = sinon.spy(function addSpy() {});
+        const testPath = getFixturePath('unlink.txt');
+        const otherDirPath = getFixturePath('other-dir');
+        const otherPath = getFixturePath('other-dir/other.txt');
+        fs.mkdirSync(otherDirPath, PERM_ARR);
+        // intentionally for this test don't write fs.writeFileSync(otherPath, 'other');
+        const watcher = chokidar_watch([testPath, otherPath], options)
+          .on(EV.UNLINK, unlinkSpy)
+          .on(EV.ADD, addSpy);
+        await waitForWatcher(watcher);
+
+        await delay();
+        await fs_unlink(testPath);
+        await waitFor([unlinkSpy]);
+
+        await delay();
+        unlinkSpy.should.have.been.calledWith(testPath);
+
+        await delay();
+        await write(testPath, 're-added');
+        await waitFor([addSpy]);
+        addSpy.should.have.been.calledWith(testPath);
+      });
+      it('should detect unlink and re-add while watching a non-existent second file in the same directory', async () => {
+        options.ignoreInitial = true;
+        const unlinkSpy = sinon.spy(function unlinkSpy() {});
+        const addSpy = sinon.spy(function addSpy() {});
+        const testPath = getFixturePath('unlink.txt');
+        const otherPath = getFixturePath('other.txt');
+        // intentionally for this test don't write fs.writeFileSync(otherPath, 'other');
+        const watcher = chokidar_watch([testPath, otherPath], options)
+          .on(EV.UNLINK, unlinkSpy)
+          .on(EV.ADD, addSpy);
+        await waitForWatcher(watcher);
+
+        await delay();
+        await fs_unlink(testPath);
+        await waitFor([unlinkSpy]);
+
+        await delay();
+        unlinkSpy.should.have.been.calledWith(testPath);
+
+        await delay();
+        await write(testPath, 're-added');
+        await waitFor([addSpy]);
+        addSpy.should.have.been.calledWith(testPath);
+      });
+      it('should detect two unlinks and one re-add', async () => {
+        options.ignoreInitial = true;
+        const unlinkSpy = sinon.spy(function unlinkSpy() {});
+        const addSpy = sinon.spy(function addSpy() {});
+        const testPath = getFixturePath('unlink.txt');
+        const otherPath = getFixturePath('other.txt');
+        fs.writeFileSync(otherPath, 'other');
+        const watcher = chokidar_watch([testPath, otherPath], options)
+          .on(EV.UNLINK, unlinkSpy)
+          .on(EV.ADD, addSpy);
+        await waitForWatcher(watcher);
+
+        await delay();
+        await fs_unlink(otherPath);
+
+        await delay();
+        await fs_unlink(testPath);
+        await waitFor([[unlinkSpy, 2]]);
+
+        await delay();
+        unlinkSpy.should.have.been.calledWith(otherPath);
+        unlinkSpy.should.have.been.calledWith(testPath);
+
+        await delay();
+        await write(testPath, 're-added');
+        await waitFor([addSpy]);
+        addSpy.should.have.been.calledWith(testPath);
+      });
+      it('should detect unlink and re-add while watching a second file and a non-existent third file', async () => {
+        options.ignoreInitial = true;
+        const unlinkSpy = sinon.spy(function unlinkSpy() {});
+        const addSpy = sinon.spy(function addSpy() {});
+        const testPath = getFixturePath('unlink.txt');
+        const otherPath = getFixturePath('other.txt');
+        const other2Path = getFixturePath('other2.txt');
+        fs.writeFileSync(otherPath, 'other');
+        // intentionally for this test don't write fs.writeFileSync(other2Path, 'other2');
+        const watcher = chokidar_watch([testPath, otherPath, other2Path], options)
+          .on(EV.UNLINK, unlinkSpy)
+          .on(EV.ADD, addSpy);
+        await waitForWatcher(watcher);
+        await delay();
+        await fs_unlink(testPath);
+
+        await waitFor([unlinkSpy]);
+        await delay();
+        unlinkSpy.should.have.been.calledWith(testPath);
+
+        await delay();
+        await write(testPath, 're-added');
+        await waitFor([addSpy]);
+        addSpy.should.have.been.calledWith(testPath);
+      });
+    });
+  });
+  describe('renamed directory', () => {
+    it('should emit `add` for a file in a renamed directory', async () => {
+      options.ignoreInitial = true;
+      const testDir = getFixturePath('subdir');
+      const testPath = getFixturePath('subdir/add.txt');
+      const renamedDir = getFixturePath('subdir-renamed');
+      const expectedPath = sysPath.join(renamedDir, 'add.txt');
+      await fs_mkdir(testDir, PERM_ARR);
+      await write(testPath, dateNow());
+      const watcher = chokidar_watch(currentDir, options);
+      const spy = await aspy(watcher, EV.ADD);
+
+      await delay(1000);
+      await fs_rename(testDir, renamedDir);
+      await waitFor([spy.withArgs(expectedPath)]);
+      spy.should.have.been.calledWith(expectedPath);
+    });
+  });
+  describe('watch non-existent paths', () => {
+    it('should watch non-existent file and detect add', async () => {
+      const testPath = getFixturePath('add.txt');
+      const watcher = chokidar_watch(testPath, options);
+      const spy = await aspy(watcher, EV.ADD);
+
+      await delay();
+      await write(testPath, dateNow());
+      await waitFor([spy]);
+      spy.should.have.been.calledWith(testPath);
+    });
+    it('should watch non-existent dir and detect addDir/add', async () => {
+      const testDir = getFixturePath('subdir');
+      const testPath = getFixturePath('subdir/add.txt');
+      const watcher = chokidar_watch(testDir, options);
+      const spy = await aspy(watcher, EV.ALL);
+      spy.should.not.have.been.called;
+
+      await delay();
+      await fs_mkdir(testDir, PERM_ARR);
+      await waitFor([spy.withArgs(EV.ADD_DIR)]);
+      await write(testPath, 'hello');
+      await waitFor([spy.withArgs(EV.ADD)]);
+      spy.should.have.been.calledWith(EV.ADD_DIR, testDir);
+      spy.should.have.been.calledWith(EV.ADD, testPath);
+    });
+  });
+  describe('not watch glob patterns', () => {
+    it('should not confuse glob-like filenames with globs', async () => {
+      const filePath = getFixturePath('nota[glob].txt');
+      await write(filePath, 'b');
+      await delay();
+      const spy = await aspy(chokidar_watch(), EV.ALL);
+      spy.should.have.been.calledWith(EV.ADD, filePath);
+
+      await delay();
+      await write(filePath, dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE, filePath)]);
+      spy.should.have.been.calledWith(EV.CHANGE, filePath);
+    });
+    it('should treat glob-like directory names as literal directory names when globbing is disabled', async () => {
+      options.disableGlobbing = true;
+      const filePath = getFixturePath('nota[glob]/a.txt');
+      const watchPath = getFixturePath('nota[glob]');
+      const testDir = getFixturePath('nota[glob]');
+      const matchingDir = getFixturePath('notag');
+      const matchingFile = getFixturePath('notag/b.txt');
+      const matchingFile2 = getFixturePath('notal');
+      fs.mkdirSync(testDir, PERM_ARR);
+      fs.writeFileSync(filePath, 'b');
+      fs.mkdirSync(matchingDir, PERM_ARR);
+      fs.writeFileSync(matchingFile, 'c');
+      fs.writeFileSync(matchingFile2, 'd');
+      const watcher = chokidar_watch(watchPath, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      spy.should.have.been.calledWith(EV.ADD, filePath);
+      spy.should.not.have.been.calledWith(EV.ADD_DIR, matchingDir);
+      spy.should.not.have.been.calledWith(EV.ADD, matchingFile);
+      spy.should.not.have.been.calledWith(EV.ADD, matchingFile2);
+      await delay();
+      await write(filePath, dateNow());
+
+      await waitFor([spy.withArgs(EV.CHANGE, filePath)]);
+      spy.should.have.been.calledWith(EV.CHANGE, filePath);
+    });
+    it('should treat glob-like filenames as literal filenames', async () => {
+      options.disableGlobbing = true;
+      const filePath = getFixturePath('nota[glob]');
+      // This isn't using getGlobPath because it isn't treated as a glob
+      const watchPath = getFixturePath('nota[glob]');
+      const matchingDir = getFixturePath('notag');
+      const matchingFile = getFixturePath('notag/a.txt');
+      const matchingFile2 = getFixturePath('notal');
+      fs.writeFileSync(filePath, 'b');
+      fs.mkdirSync(matchingDir, PERM_ARR);
+      fs.writeFileSync(matchingFile, 'c');
+      fs.writeFileSync(matchingFile2, 'd');
+      const watcher = chokidar_watch(watchPath, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      spy.should.have.been.calledWith(EV.ADD, filePath);
+      spy.should.not.have.been.calledWith(EV.ADD_DIR, matchingDir);
+      spy.should.not.have.been.calledWith(EV.ADD, matchingFile);
+      spy.should.not.have.been.calledWith(EV.ADD, matchingFile2);
+      await delay();
+      await write(filePath, dateNow());
+
+      await waitFor([spy.withArgs(EV.CHANGE, filePath)]);
+      spy.should.have.been.calledWith(EV.CHANGE, filePath);
+    });
+  });
+  describe('watch symlinks', () => {
+    if (isWindows) return true;
+    let linkedDir;
+    beforeEach(async () => {
+      linkedDir = sysPath.resolve(currentDir, '..', `${subdirId}-link`);
+      await fs_symlink(currentDir, linkedDir, isWindows ? 'dir' : null);
+      await fs_mkdir(getFixturePath('subdir'), PERM_ARR);
+      await write(getFixturePath('subdir/add.txt'), 'b');
+      return true;
+    });
+    afterEach(async () => {
+      await fs_unlink(linkedDir);
+      return true;
+    });
+
+    it('should watch symlinked dirs', async () => {
+      const dirSpy = sinon.spy(function dirSpy() {});
+      const addSpy = sinon.spy(function addSpy() {});
+      const watcher = chokidar_watch(linkedDir, options).on(EV.ADD_DIR, dirSpy).on(EV.ADD, addSpy);
+      await waitForWatcher(watcher);
+
+      dirSpy.should.have.been.calledWith(linkedDir);
+      addSpy.should.have.been.calledWith(sysPath.join(linkedDir, 'change.txt'));
+      addSpy.should.have.been.calledWith(sysPath.join(linkedDir, 'unlink.txt'));
+    });
+    it('should watch symlinked files', async () => {
+      const changePath = getFixturePath('change.txt');
+      const linkPath = getFixturePath('link.txt');
+      fs.symlinkSync(changePath, linkPath);
+      const watcher = chokidar_watch(linkPath, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      await write(changePath, dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE)]);
+      spy.should.have.been.calledWith(EV.ADD, linkPath);
+      spy.should.have.been.calledWith(EV.CHANGE, linkPath);
+    });
+    it('should follow symlinked files within a normal dir', async () => {
+      const changePath = getFixturePath('change.txt');
+      const linkPath = getFixturePath('subdir/link.txt');
+      fs.symlinkSync(changePath, linkPath);
+      const watcher = chokidar_watch(getFixturePath('subdir'), options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      await write(changePath, dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE, linkPath)]);
+      spy.should.have.been.calledWith(EV.ADD, linkPath);
+      spy.should.have.been.calledWith(EV.CHANGE, linkPath);
+    });
+    it('should watch paths with a symlinked parent', async () => {
+      const testDir = sysPath.join(linkedDir, 'subdir');
+      const testFile = sysPath.join(testDir, 'add.txt');
+      const watcher = chokidar_watch(testDir, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      spy.should.have.been.calledWith(EV.ADD_DIR, testDir);
+      spy.should.have.been.calledWith(EV.ADD, testFile);
+      await write(getFixturePath('subdir/add.txt'), dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE)]);
+      spy.should.have.been.calledWith(EV.CHANGE, testFile);
+    });
+    it('should not recurse indefinitely on circular symlinks', async () => {
+      await fs_symlink(currentDir, getFixturePath('subdir/circular'), isWindows ? 'dir' : null);
+      return new Promise((resolve, reject) => {
+        const watcher = chokidar_watch();
+        watcher.on(EV.ERROR, resolve());
+        watcher.on(
+          EV.READY,
+          reject('The watcher becomes ready, although he watches a circular symlink.')
+        );
+      });
+    });
+    it('should recognize changes following symlinked dirs', async () => {
+      const linkedFilePath = sysPath.join(linkedDir, 'change.txt');
+      const watcher = chokidar_watch(linkedDir, options);
+      const spy = await aspy(watcher, EV.CHANGE);
+      const wa = spy.withArgs(linkedFilePath);
+      await write(getFixturePath('change.txt'), dateNow());
+      await waitFor([wa]);
+      spy.should.have.been.calledWith(linkedFilePath);
+    });
+    it('should follow newly created symlinks', async () => {
+      options.ignoreInitial = true;
+      const watcher = chokidar_watch();
+      const spy = await aspy(watcher, EV.ALL);
+      await delay();
+      await fs_symlink(getFixturePath('subdir'), getFixturePath('link'), isWindows ? 'dir' : null);
+      await waitFor([
+        spy.withArgs(EV.ADD, getFixturePath('link/add.txt')),
+        spy.withArgs(EV.ADD_DIR, getFixturePath('link')),
+      ]);
+      spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('link'));
+      spy.should.have.been.calledWith(EV.ADD, getFixturePath('link/add.txt'));
+    });
+    it('should watch symlinks as files when followSymlinks:false', async () => {
+      options.followSymlinks = false;
+      const watcher = chokidar_watch(linkedDir, options);
+      const spy = await aspy(watcher, EV.ALL);
+      spy.should.not.have.been.calledWith(EV.ADD_DIR);
+      spy.should.have.been.calledWith(EV.ADD, linkedDir);
+      spy.should.have.been.calledOnce;
+    });
+    it('should survive ENOENT for missing symlinks when followSymlinks:false', async () => {
+      options.followSymlinks = false;
+      const targetDir = getFixturePath('subdir/nonexistent');
+      await fs_mkdir(targetDir);
+      await fs_symlink(targetDir, getFixturePath('subdir/broken'), isWindows ? 'dir' : null);
+      await fs_rmdir(targetDir);
+      await delay();
+
+      const watcher = chokidar_watch(getFixturePath('subdir'), options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      spy.should.have.been.calledTwice;
+      spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('subdir'));
+      spy.should.have.been.calledWith(EV.ADD, getFixturePath('subdir/add.txt'));
+    });
+    it('should watch symlinks within a watched dir as files when followSymlinks:false', async () => {
+      options.followSymlinks = false;
+      // Create symlink in linkPath
+      const linkPath = getFixturePath('link');
+      fs.symlinkSync(getFixturePath('subdir'), linkPath);
+      const spy = await aspy(chokidar_watch(), EV.ALL);
+      await delay(300);
+      setTimeout(
+        () => {
+          fs.writeFileSync(getFixturePath('subdir/add.txt'), dateNow());
+          fs.unlinkSync(linkPath);
+          fs.symlinkSync(getFixturePath('subdir/add.txt'), linkPath);
+        },
+        options.usePolling ? 1200 : 300
+      );
+
+      await delay(300);
+      await waitFor([spy.withArgs(EV.CHANGE, linkPath)]);
+      spy.should.not.have.been.calledWith(EV.ADD_DIR, linkPath);
+      spy.should.not.have.been.calledWith(EV.ADD, getFixturePath('link/add.txt'));
+      spy.should.have.been.calledWith(EV.ADD, linkPath);
+      spy.should.have.been.calledWith(EV.CHANGE, linkPath);
+    });
+    it('should not reuse watcher when following a symlink to elsewhere', async () => {
+      const linkedPath = getFixturePath('outside');
+      const linkedFilePath = sysPath.join(linkedPath, 'text.txt');
+      const linkPath = getFixturePath('subdir/subsub');
+      fs.mkdirSync(linkedPath, PERM_ARR);
+      fs.writeFileSync(linkedFilePath, 'b');
+      fs.symlinkSync(linkedPath, linkPath);
+      const watcher2 = chokidar_watch(getFixturePath('subdir'), options);
+      await waitForWatcher(watcher2);
+
+      await delay(options.usePolling ? 900 : undefined);
+      const watchedPath = getFixturePath('subdir/subsub/text.txt');
+      const watcher = chokidar_watch(watchedPath, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      await delay();
+      await write(linkedFilePath, dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE)]);
+      spy.should.have.been.calledWith(EV.CHANGE, watchedPath);
+    });
+    it('should emit ready event even when broken symlinks are encountered', async () => {
+      const targetDir = getFixturePath('subdir/nonexistent');
+      await fs_mkdir(targetDir);
+      await fs_symlink(targetDir, getFixturePath('subdir/broken'), isWindows ? 'dir' : null);
+      await fs_rmdir(targetDir);
+      const readySpy = sinon.spy(function readySpy() {});
+      const watcher = chokidar_watch(getFixturePath('subdir'), options).on(EV.READY, readySpy);
+      await waitForWatcher(watcher);
+      readySpy.should.have.been.calledOnce;
+    });
+  });
+  describe('watch arrays of paths/globs', () => {
+    it('should watch all paths in an array', async () => {
+      const testPath = getFixturePath('change.txt');
+      const testDir = getFixturePath('subdir');
+      fs.mkdirSync(testDir);
+      const watcher = chokidar_watch([testDir, testPath], options);
+      const spy = await aspy(watcher, EV.ALL);
+      spy.should.have.been.calledWith(EV.ADD, testPath);
+      spy.should.have.been.calledWith(EV.ADD_DIR, testDir);
+      spy.should.not.have.been.calledWith(EV.ADD, getFixturePath('unlink.txt'));
+      await write(testPath, dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE)]);
+      spy.should.have.been.calledWith(EV.CHANGE, testPath);
+    });
+    it('should accommodate nested arrays in input', async () => {
+      const testPath = getFixturePath('change.txt');
+      const testDir = getFixturePath('subdir');
+      await fs_mkdir(testDir);
+      const watcher = chokidar_watch([[testDir], [testPath]], options);
+      const spy = await aspy(watcher, EV.ALL);
+      spy.should.have.been.calledWith(EV.ADD, testPath);
+      spy.should.have.been.calledWith(EV.ADD_DIR, testDir);
+      spy.should.not.have.been.calledWith(EV.ADD, getFixturePath('unlink.txt'));
+      await write(testPath, dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE)]);
+      spy.should.have.been.calledWith(EV.CHANGE, testPath);
+    });
+    it('should throw if provided any non-string paths', () => {
+      expect(chokidar_watch.bind(null, [[currentDir], /notastring/])).to.throw(
+        TypeError,
+        /non-string/i
+      );
+    });
+  });
+  describe('watch options', () => {
+    describe('ignoreInitial', () => {
+      describe('false', () => {
+        beforeEach(() => {
+          options.ignoreInitial = false;
+        });
+        it('should emit `add` events for preexisting files', async () => {
+          const watcher = chokidar_watch(currentDir, options);
+          const spy = await aspy(watcher, EV.ADD);
+          spy.should.have.been.calledTwice;
+        });
+        it('should emit `addDir` event for watched dir', async () => {
+          const watcher = chokidar_watch(currentDir, options);
+          const spy = await aspy(watcher, EV.ADD_DIR);
+          spy.should.have.been.calledOnce;
+          spy.should.have.been.calledWith(currentDir);
+        });
+        it('should emit `addDir` events for preexisting dirs', async () => {
+          await fs_mkdir(getFixturePath('subdir'), PERM_ARR);
+          await fs_mkdir(getFixturePath('subdir/subsub'), PERM_ARR);
+          const watcher = chokidar_watch(currentDir, options);
+          const spy = await aspy(watcher, EV.ADD_DIR);
+          spy.should.have.been.calledWith(currentDir);
+          spy.should.have.been.calledWith(getFixturePath('subdir'));
+          spy.should.have.been.calledWith(getFixturePath('subdir/subsub'));
+          spy.should.have.been.calledThrice;
+        });
+      });
+      describe('true', () => {
+        beforeEach(() => {
+          options.ignoreInitial = true;
+        });
+        it('should ignore initial add events', async () => {
+          const watcher = chokidar_watch();
+          const spy = await aspy(watcher, EV.ADD);
+          await delay();
+          spy.should.not.have.been.called;
+        });
+        it('should ignore add events on a subsequent .add()', async () => {
+          const watcher = chokidar_watch(getFixturePath('subdir'), options);
+          const spy = await aspy(watcher, EV.ADD);
+          watcher.add(currentDir);
+          await delay(1000);
+          spy.should.not.have.been.called;
+        });
+        it('should notice when a file appears in an empty directory', async () => {
+          const testDir = getFixturePath('subdir');
+          const testPath = getFixturePath('subdir/add.txt');
+          const spy = await aspy(chokidar_watch(), EV.ADD);
+          spy.should.not.have.been.called;
+          await fs_mkdir(testDir, PERM_ARR);
+          await write(testPath, dateNow());
+          await waitFor([spy]);
+          spy.should.have.been.calledOnce;
+          spy.should.have.been.calledWith(testPath);
+        });
+        it('should emit a change on a preexisting file as a change', async () => {
+          const testPath = getFixturePath('change.txt');
+          const spy = await aspy(chokidar_watch(), EV.ALL);
+          spy.should.not.have.been.called;
+          await write(testPath, dateNow());
+          await waitFor([spy.withArgs(EV.CHANGE, testPath)]);
+          spy.should.have.been.calledWith(EV.CHANGE, testPath);
+          spy.should.not.have.been.calledWith(EV.ADD);
+        });
+        it('should not emit for preexisting dirs when depth is 0', async () => {
+          options.depth = 0;
+          const testPath = getFixturePath('add.txt');
+          await fs_mkdir(getFixturePath('subdir'), PERM_ARR);
+
+          await delay(200);
+          const spy = await aspy(chokidar_watch(), EV.ALL);
+          await write(testPath, dateNow());
+          await waitFor([spy]);
+
+          await delay(200);
+          spy.should.have.been.calledWith(EV.ADD, testPath);
+          spy.should.not.have.been.calledWith(EV.ADD_DIR);
+        });
+      });
+    });
+    describe('ignored', () => {
+      it('should check ignore after stating', async () => {
+        options.ignored = (path, stats) => {
+          if (upath.normalizeSafe(path) === upath.normalizeSafe(testDir) || !stats) return false;
+          return stats.isDirectory();
+        };
+        const testDir = getFixturePath('subdir');
+        fs.mkdirSync(testDir, PERM_ARR);
+        fs.writeFileSync(sysPath.join(testDir, 'add.txt'), '');
+        fs.mkdirSync(sysPath.join(testDir, 'subsub'), PERM_ARR);
+        fs.writeFileSync(sysPath.join(testDir, 'subsub', 'ab.txt'), '');
+        const watcher = chokidar_watch(testDir, options);
+        const spy = await aspy(watcher, EV.ADD);
+        spy.should.have.been.calledOnce;
+        spy.should.have.been.calledWith(sysPath.join(testDir, 'add.txt'));
+      });
+      it('should not choke on an ignored watch path', async () => {
+        options.ignored = () => {
+          return true;
+        };
+        await waitForWatcher(chokidar_watch());
+      });
+      it('should ignore the contents of ignored dirs', async () => {
+        const testDir = getFixturePath('subdir');
+        const testFile = sysPath.join(testDir, 'add.txt');
+        options.ignored = testDir;
+        fs.mkdirSync(testDir, PERM_ARR);
+        fs.writeFileSync(testFile, 'b');
+        const watcher = chokidar_watch(currentDir, options);
+        const spy = await aspy(watcher, EV.ALL);
+
+        await delay();
+        await write(testFile, dateNow());
+
+        await delay(300);
+        spy.should.not.have.been.calledWith(EV.ADD_DIR, testDir);
+        spy.should.not.have.been.calledWith(EV.ADD, testFile);
+        spy.should.not.have.been.calledWith(EV.CHANGE, testFile);
+      });
+      it('should allow regex/fn ignores', async () => {
+        options.cwd = currentDir;
+        options.ignored = /add/;
+
+        fs.writeFileSync(getFixturePath('add.txt'), 'b');
+        const watcher = chokidar_watch(currentDir, options);
+        const spy = await aspy(watcher, EV.ALL);
+
+        await delay();
+        await write(getFixturePath('add.txt'), dateNow());
+        await write(getFixturePath('change.txt'), dateNow());
+
+        await waitFor([spy.withArgs(EV.CHANGE, 'change.txt')]);
+        spy.should.not.have.been.calledWith(EV.ADD, 'add.txt');
+        spy.should.not.have.been.calledWith(EV.CHANGE, 'add.txt');
+        spy.should.have.been.calledWith(EV.ADD, 'change.txt');
+        spy.should.have.been.calledWith(EV.CHANGE, 'change.txt');
+      });
+    });
+    describe('depth', () => {
+      beforeEach(async () => {
+        await fs_mkdir(getFixturePath('subdir'), PERM_ARR);
+        await write(getFixturePath('subdir/add.txt'), 'b');
+        await delay();
+        await fs_mkdir(getFixturePath('subdir/subsub'), PERM_ARR);
+        await write(getFixturePath('subdir/subsub/ab.txt'), 'b');
+        await delay();
+      });
+      it('should not recurse if depth is 0', async () => {
+        options.depth = 0;
+        const watcher = chokidar_watch();
+        const spy = await aspy(watcher, EV.ALL);
+        await write(getFixturePath('subdir/add.txt'), dateNow());
+        await waitFor([[spy, 4]]);
+        spy.should.have.been.calledWith(EV.ADD_DIR, currentDir);
+        spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('subdir'));
+        spy.should.have.been.calledWith(EV.ADD, getFixturePath('change.txt'));
+        spy.should.have.been.calledWith(EV.ADD, getFixturePath('unlink.txt'));
+        spy.should.not.have.been.calledWith(EV.CHANGE);
+        if (!macosFswatch) spy.callCount.should.equal(4);
+      });
+      it('should recurse to specified depth', async () => {
+        options.depth = 1;
+        const addPath = getFixturePath('subdir/add.txt');
+        const changePath = getFixturePath('change.txt');
+        const ignoredPath = getFixturePath('subdir/subsub/ab.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await delay();
+        await write(getFixturePath('change.txt'), dateNow());
+        await write(addPath, dateNow());
+        await write(ignoredPath, dateNow());
+        await waitFor([spy.withArgs(EV.CHANGE, addPath), spy.withArgs(EV.CHANGE, changePath)]);
+        spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('subdir/subsub'));
+        spy.should.have.been.calledWith(EV.CHANGE, changePath);
+        spy.should.have.been.calledWith(EV.CHANGE, addPath);
+        spy.should.not.have.been.calledWith(EV.ADD, ignoredPath);
+        spy.should.not.have.been.calledWith(EV.CHANGE, ignoredPath);
+        if (!macosFswatch) spy.callCount.should.equal(8);
+      });
+      it('should respect depth setting when following symlinks', async () => {
+        if (isWindows) return true; // skip on windows
+        options.depth = 1;
+        await fs_symlink(
+          getFixturePath('subdir'),
+          getFixturePath('link'),
+          isWindows ? 'dir' : null
+        );
+        await delay();
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('link'));
+        spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('link/subsub'));
+        spy.should.have.been.calledWith(EV.ADD, getFixturePath('link/add.txt'));
+        spy.should.not.have.been.calledWith(EV.ADD, getFixturePath('link/subsub/ab.txt'));
+      });
+      it('should respect depth setting when following a new symlink', async () => {
+        if (isWindows) return true; // skip on windows
+        options.depth = 1;
+        options.ignoreInitial = true;
+        const linkPath = getFixturePath('link');
+        const dirPath = getFixturePath('link/subsub');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await fs_symlink(getFixturePath('subdir'), linkPath, isWindows ? 'dir' : null);
+        await waitFor([[spy, 3], spy.withArgs(EV.ADD_DIR, dirPath)]);
+        spy.should.have.been.calledWith(EV.ADD_DIR, linkPath);
+        spy.should.have.been.calledWith(EV.ADD_DIR, dirPath);
+        spy.should.have.been.calledWith(EV.ADD, getFixturePath('link/add.txt'));
+        spy.should.have.been.calledThrice;
+      });
+      it('should correctly handle dir events when depth is 0', async () => {
+        options.depth = 0;
+        const subdir2 = getFixturePath('subdir2');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        const addSpy = spy.withArgs(EV.ADD_DIR);
+        const unlinkSpy = spy.withArgs(EV.UNLINK_DIR);
+        spy.should.have.been.calledWith(EV.ADD_DIR, currentDir);
+        spy.should.have.been.calledWith(EV.ADD_DIR, getFixturePath('subdir'));
+        await fs_mkdir(subdir2, PERM_ARR);
+        await waitFor([[addSpy, 3]]);
+        addSpy.should.have.been.calledThrice;
+
+        await fs_rmdir(subdir2);
+        await waitFor([unlinkSpy]);
+        await delay();
+        unlinkSpy.should.have.been.calledWith(EV.UNLINK_DIR, subdir2);
+        unlinkSpy.should.have.been.calledOnce;
+      });
+    });
+    describe('atomic', () => {
+      beforeEach(() => {
+        options.atomic = true;
+        options.ignoreInitial = true;
+      });
+      it('should ignore vim/emacs/Sublime swapfiles', async () => {
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(getFixturePath('.change.txt.swp'), 'a'); // vim
+        await write(getFixturePath('add.txt~'), 'a'); // vim/emacs
+        await write(getFixturePath('.subl5f4.tmp'), 'a'); // sublime
+        await delay(300);
+        await write(getFixturePath('.change.txt.swp'), 'c');
+        await write(getFixturePath('add.txt~'), 'c');
+        await write(getFixturePath('.subl5f4.tmp'), 'c');
+        await delay(300);
+        await fs_unlink(getFixturePath('.change.txt.swp'));
+        await fs_unlink(getFixturePath('add.txt~'));
+        await fs_unlink(getFixturePath('.subl5f4.tmp'));
+        await delay(300);
+        spy.should.not.have.been.called;
+      });
+      it('should ignore stale tilde files', async () => {
+        options.ignoreInitial = false;
+        await write(getFixturePath('old.txt~'), 'a');
+        await delay();
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        spy.should.not.have.been.calledWith(getFixturePath('old.txt'));
+        spy.should.not.have.been.calledWith(getFixturePath('old.txt~'));
+      });
+    });
+    describe('cwd', () => {
+      it('should emit relative paths based on cwd', async () => {
+        options.cwd = currentDir;
+        const watcher = chokidar_watch('.', options);
+        const spy = await aspy(watcher, EV.ALL);
+        await fs_unlink(getFixturePath('unlink.txt'));
+        await write(getFixturePath('change.txt'), dateNow());
+        await waitFor([spy.withArgs(EV.UNLINK)]);
+        spy.should.have.been.calledWith(EV.ADD, 'change.txt');
+        spy.should.have.been.calledWith(EV.ADD, 'unlink.txt');
+        spy.should.have.been.calledWith(EV.CHANGE, 'change.txt');
+        spy.should.have.been.calledWith(EV.UNLINK, 'unlink.txt');
+      });
+      it('should emit `addDir` with alwaysStat for renamed directory', async () => {
+        options.cwd = currentDir;
+        options.alwaysStat = true;
+        options.ignoreInitial = true;
+        const spy = sinon.spy();
+        const testDir = getFixturePath('subdir');
+        const renamedDir = getFixturePath('subdir-renamed');
+
+        await fs_mkdir(testDir, PERM_ARR);
+        const watcher = chokidar_watch('.', options);
+
+        setTimeout(() => {
+          watcher.on(EV.ADD_DIR, spy);
+          fs_rename(testDir, renamedDir);
+        }, 1000);
+
+        await waitFor([spy]);
+        spy.should.have.been.calledOnce;
+        spy.should.have.been.calledWith('subdir-renamed');
+        expect(spy.args[0][1]).to.be.ok; // stats
+      });
+      it('should allow separate watchers to have different cwds', async () => {
+        options.cwd = currentDir;
+        const options2 = {};
+        Object.keys(options).forEach((key) => {
+          options2[key] = options[key];
+        });
+        options2.cwd = getFixturePath('subdir');
+        const watcher = chokidar_watch(getGlobPath('.'), options);
+        const watcherEvents = waitForEvents(watcher, 3);
+        const spy1 = await aspy(watcher, EV.ALL);
+
+        await delay();
+        const watcher2 = chokidar_watch(currentDir, options2);
+        const watcher2Events = waitForEvents(watcher2, 5);
+        const spy2 = await aspy(watcher2, EV.ALL);
+
+        await fs_unlink(getFixturePath('unlink.txt'));
+        await write(getFixturePath('change.txt'), dateNow());
+        await Promise.all([watcherEvents, watcher2Events]);
+        spy1.should.have.been.calledWith(EV.CHANGE, 'change.txt');
+        spy1.should.have.been.calledWith(EV.UNLINK, 'unlink.txt');
+        spy2.should.have.been.calledWith(EV.ADD, sysPath.join('..', 'change.txt'));
+        spy2.should.have.been.calledWith(EV.ADD, sysPath.join('..', 'unlink.txt'));
+        spy2.should.have.been.calledWith(EV.CHANGE, sysPath.join('..', 'change.txt'));
+        spy2.should.have.been.calledWith(EV.UNLINK, sysPath.join('..', 'unlink.txt'));
+      });
+      it('should ignore files even with cwd', async () => {
+        options.cwd = currentDir;
+        options.ignored = ['ignored-option.txt', 'ignored.txt'];
+        const files = ['.'];
+        fs.writeFileSync(getFixturePath('change.txt'), 'hello');
+        fs.writeFileSync(getFixturePath('ignored.txt'), 'ignored');
+        fs.writeFileSync(getFixturePath('ignored-option.txt'), 'ignored option');
+        const watcher = chokidar_watch(files, options);
+
+        const spy = await aspy(watcher, EV.ALL);
+        fs.writeFileSync(getFixturePath('ignored.txt'), dateNow());
+        fs.writeFileSync(getFixturePath('ignored-option.txt'), dateNow());
+        await fs_unlink(getFixturePath('ignored.txt'));
+        await fs_unlink(getFixturePath('ignored-option.txt'));
+        await delay();
+        await write(getFixturePath('change.txt'), EV.CHANGE);
+        await waitFor([spy.withArgs(EV.CHANGE, 'change.txt')]);
+        spy.should.have.been.calledWith(EV.ADD, 'change.txt');
+        spy.should.not.have.been.calledWith(EV.ADD, 'ignored.txt');
+        spy.should.not.have.been.calledWith(EV.ADD, 'ignored-option.txt');
+        spy.should.not.have.been.calledWith(EV.CHANGE, 'ignored.txt');
+        spy.should.not.have.been.calledWith(EV.CHANGE, 'ignored-option.txt');
+        spy.should.not.have.been.calledWith(EV.UNLINK, 'ignored.txt');
+        spy.should.not.have.been.calledWith(EV.UNLINK, 'ignored-option.txt');
+        spy.should.have.been.calledWith(EV.CHANGE, 'change.txt');
+      });
+    });
+    describe('ignorePermissionErrors', () => {
+      let filePath;
+      beforeEach(async () => {
+        filePath = getFixturePath('add.txt');
+        await write(filePath, 'b', { mode: 128 });
+        await delay();
+      });
+      describe('false', () => {
+        beforeEach(() => {
+          options.ignorePermissionErrors = false;
+          // chokidar_watch();
+        });
+        it('should not watch files without read permissions', async () => {
+          if (isWindows) return true;
+          const spy = await aspy(chokidar_watch(), EV.ALL);
+          spy.should.not.have.been.calledWith(EV.ADD, filePath);
+          await write(filePath, dateNow());
+
+          await delay(200);
+          spy.should.not.have.been.calledWith(EV.CHANGE, filePath);
+        });
+      });
+      describe('true', () => {
+        beforeEach(() => {
+          options.ignorePermissionErrors = true;
+        });
+        it('should watch unreadable files if possible', async () => {
+          const spy = await aspy(chokidar_watch(), EV.ALL);
+          spy.should.have.been.calledWith(EV.ADD, filePath);
+        });
+        it('should not choke on non-existent files', async () => {
+          const watcher = chokidar_watch(getFixturePath('nope.txt'), options);
+          await waitForWatcher(watcher);
+        });
+      });
+    });
+    describe('awaitWriteFinish', () => {
+      beforeEach(() => {
+        options.awaitWriteFinish = { stabilityThreshold: 500 };
+        options.ignoreInitial = true;
+      });
+      it('should use default options if none given', () => {
+        options.awaitWriteFinish = true;
+        const watcher = chokidar_watch();
+        expect(watcher.options.awaitWriteFinish.pollInterval).to.equal(100);
+        expect(watcher.options.awaitWriteFinish.stabilityThreshold).to.equal(2000);
+      });
+      it('should not emit add event before a file is fully written', async () => {
+        const testPath = getFixturePath('add.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(testPath, 'hello');
+        await delay(200);
+        spy.should.not.have.been.calledWith(EV.ADD);
+      });
+      it('should wait for the file to be fully written before emitting the add event', async () => {
+        const testPath = getFixturePath('add.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(testPath, 'hello');
+
+        await delay(300);
+        spy.should.not.have.been.called;
+        await waitFor([spy]);
+        spy.should.have.been.calledWith(EV.ADD, testPath);
+      });
+      it('should emit with the final stats', async () => {
+        const testPath = getFixturePath('add.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(testPath, 'hello ');
+
+        await delay(300);
+        fs.appendFileSync(testPath, 'world!');
+
+        await waitFor([spy]);
+        spy.should.have.been.calledWith(EV.ADD, testPath);
+        expect(spy.args[0][2].size).to.equal(12);
+      });
+      it('should not emit change event while a file has not been fully written', async () => {
+        const testPath = getFixturePath('add.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(testPath, 'hello');
+        await delay(100);
+        await write(testPath, 'edit');
+        await delay(200);
+        spy.should.not.have.been.calledWith(EV.CHANGE, testPath);
+      });
+      it('should not emit change event before an existing file is fully updated', async () => {
+        const testPath = getFixturePath('change.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(testPath, 'hello');
+        await delay(300);
+        spy.should.not.have.been.calledWith(EV.CHANGE, testPath);
+      });
+      it('should wait for an existing file to be fully updated before emitting the change event', async () => {
+        const testPath = getFixturePath('change.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        fs.writeFile(testPath, 'hello', () => {});
+
+        await delay(300);
+        spy.should.not.have.been.called;
+        await waitFor([spy]);
+        spy.should.have.been.calledWith(EV.CHANGE, testPath);
+      });
+      it('should emit change event after the file is fully written', async () => {
+        const testPath = getFixturePath('add.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await delay();
+        await write(testPath, 'hello');
+
+        await waitFor([spy]);
+        spy.should.have.been.calledWith(EV.ADD, testPath);
+        await write(testPath, 'edit');
+        await waitFor([spy.withArgs(EV.CHANGE)]);
+        spy.should.have.been.calledWith(EV.CHANGE, testPath);
+      });
+      it('should not raise any event for a file that was deleted before fully written', async () => {
+        const testPath = getFixturePath('add.txt');
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(testPath, 'hello');
+        await delay(400);
+        await fs_unlink(testPath);
+        await delay(400);
+        spy.should.not.have.been.calledWith(sinon.match.string, testPath);
+      });
+      it('should be compatible with the cwd option', async () => {
+        const testPath = getFixturePath('subdir/add.txt');
+        const filename = sysPath.basename(testPath);
+        options.cwd = sysPath.dirname(testPath);
+        await fs_mkdir(options.cwd);
+
+        await delay(200);
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+
+        await delay(400);
+        await write(testPath, 'hello');
+
+        await waitFor([spy.withArgs(EV.ADD)]);
+        spy.should.have.been.calledWith(EV.ADD, filename);
+      });
+      it('should still emit initial add events', async () => {
+        options.ignoreInitial = false;
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        spy.should.have.been.calledWith(EV.ADD);
+        spy.should.have.been.calledWith(EV.ADD_DIR);
+      });
+      it('should emit an unlink event when a file is updated and deleted just after that', async () => {
+        const testPath = getFixturePath('subdir/add.txt');
+        const filename = sysPath.basename(testPath);
+        options.cwd = sysPath.dirname(testPath);
+        await fs_mkdir(options.cwd);
+        await delay();
+        await write(testPath, 'hello');
+        await delay();
+        const spy = await aspy(chokidar_watch(), EV.ALL);
+        await write(testPath, 'edit');
+        await delay();
+        await fs_unlink(testPath);
+        await waitFor([spy.withArgs(EV.UNLINK)]);
+        spy.should.have.been.calledWith(EV.UNLINK, filename);
+        spy.should.not.have.been.calledWith(EV.CHANGE, filename);
+      });
+      describe('race condition', () => {
+        function w(fn, to) {
+          return setTimeout.bind(null, fn, to || slowerDelay || 50);
+        }
+        function simpleCb(err) {
+          if (err) throw err;
+        }
+
+        // Reproduces bug https://github.com/paulmillr/chokidar/issues/546, which was causing an
+        // uncaught exception. The race condition is likelier to happen when stat() is slow.
+        const _realStat = fs.stat;
+        beforeEach(() => {
+          options.awaitWriteFinish = { pollInterval: 50, stabilityThreshold: 50 };
+          options.ignoreInitial = true;
+
+          // Stub fs.stat() to take a while to return.
+          sinon.stub(fs, 'stat').callsFake((path, cb) => {
+            _realStat(path, w(cb, 250));
+          });
+        });
+
+        afterEach(() => {
+          // Restore fs.stat() back to normal.
+          sinon.restore();
+        });
+
+        function _waitFor(spies, fn) {
+          function isSpyReady(spy) {
+            return Array.isArray(spy) ? spy[0].callCount >= spy[1] : spy.callCount;
+          }
+          // eslint-disable-next-line prefer-const
+          let intrvl;
+          // eslint-disable-next-line prefer-const
+          let to;
+          function finish() {
+            clearInterval(intrvl);
+            clearTimeout(to);
+            fn();
+            fn = Function.prototype;
+          }
+          intrvl = setInterval(() => {
+            if (spies.every(isSpyReady)) finish();
+          }, 5);
+          to = setTimeout(finish, 3500);
+        }
+
+        it('should handle unlink that happens while waiting for stat to return', (done) => {
+          const spy = sinon.spy();
+          const testPath = getFixturePath('add.txt');
+          chokidar_watch()
+            .on(EV.ALL, spy)
+            .on(EV.READY, () => {
+              fs.writeFile(testPath, 'hello', simpleCb);
+              _waitFor([spy], () => {
+                spy.should.have.been.calledWith(EV.ADD, testPath);
+                fs.stat.resetHistory();
+                fs.writeFile(testPath, 'edit', simpleCb);
+                w(() => {
+                  // There will be a stat() call after we notice the change, plus pollInterval.
+                  // After waiting a bit less, wait specifically for that stat() call.
+                  fs.stat.resetHistory();
+                  _waitFor([fs.stat], () => {
+                    // Once stat call is made, it will take some time to return. Meanwhile, unlink
+                    // the file and wait for that to be noticed.
+                    fs.unlink(testPath, simpleCb);
+                    _waitFor(
+                      [spy.withArgs(EV.UNLINK)],
+                      w(() => {
+                        // Wait a while after unlink to ensure stat() had time to return. That's where
+                        // an uncaught exception used to happen.
+                        spy.should.have.been.calledWith(EV.UNLINK, testPath);
+                        spy.should.not.have.been.calledWith(EV.CHANGE);
+                        done();
+                      }, 400)
+                    );
+                  });
+                }, 40)();
+              });
+            });
+        });
+      });
+    });
+  });
+  describe('getWatched', () => {
+    it('should return the watched paths', async () => {
+      const expected = {};
+      expected[sysPath.dirname(currentDir)] = [subdirId.toString()];
+      expected[currentDir] = ['change.txt', 'unlink.txt'];
+      const watcher = chokidar_watch();
+      await waitForWatcher(watcher);
+      expect(watcher.getWatched()).to.deep.equal(expected);
+    });
+    it('should set keys relative to cwd & include added paths', async () => {
+      options.cwd = currentDir;
+      const expected = {
+        '.': ['change.txt', 'subdir', 'unlink.txt'],
+        '..': [subdirId.toString()],
+        subdir: [],
+      };
+      await fs_mkdir(getFixturePath('subdir'), PERM_ARR);
+      const watcher = chokidar_watch();
+      await waitForWatcher(watcher);
+      expect(watcher.getWatched()).to.deep.equal(expected);
+    });
+  });
+  describe('unwatch', () => {
+    beforeEach(async () => {
+      options.ignoreInitial = true;
+      await fs_mkdir(getFixturePath('subdir'), PERM_ARR);
+      await delay();
+    });
+    it('should stop watching unwatched paths', async () => {
+      const watchPaths = [getFixturePath('subdir'), getFixturePath('change.txt')];
+      const watcher = chokidar_watch(watchPaths, options);
+      const spy = await aspy(watcher, EV.ALL);
+      watcher.unwatch(getFixturePath('subdir'));
+
+      await delay();
+      await write(getFixturePath('subdir/add.txt'), dateNow());
+      await write(getFixturePath('change.txt'), dateNow());
+      await waitFor([spy]);
+
+      await delay(300);
+      spy.should.have.been.calledWith(EV.CHANGE, getFixturePath('change.txt'));
+      spy.should.not.have.been.calledWith(EV.ADD);
+      if (!macosFswatch) spy.should.have.been.calledOnce;
+    });
+    it('should ignore unwatched paths that are a subset of watched paths', async () => {
+      const subdirRel = upath.relative(process.cwd(), getFixturePath('subdir'));
+      const unlinkFile = getFixturePath('unlink.txt');
+      const addFile = getFixturePath('subdir/add.txt');
+      const changedFile = getFixturePath('change.txt');
+      const watcher = chokidar_watch(currentDir, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      // test with both relative and absolute paths
+      watcher.unwatch([subdirRel, getGlobPath('unlink.txt')]);
+
+      await delay();
+      await fs_unlink(unlinkFile);
+      await write(addFile, dateNow());
+      await write(changedFile, dateNow());
+      await waitFor([spy.withArgs(EV.CHANGE)]);
+
+      await delay(300);
+      spy.should.have.been.calledWith(EV.CHANGE, changedFile);
+      spy.should.not.have.been.calledWith(EV.ADD, addFile);
+      spy.should.not.have.been.calledWith(EV.UNLINK, unlinkFile);
+      if (!macosFswatch) spy.should.have.been.calledOnce;
+    });
+    it('should unwatch relative paths', async () => {
+      const fixturesDir = sysPath.relative(process.cwd(), currentDir);
+      const subdir = sysPath.join(fixturesDir, 'subdir');
+      const changeFile = sysPath.join(fixturesDir, 'change.txt');
+      const watchPaths = [subdir, changeFile];
+      const watcher = chokidar_watch(watchPaths, options);
+      const spy = await aspy(watcher, EV.ALL);
+
+      await delay();
+      watcher.unwatch(subdir);
+      await write(getFixturePath('subdir/add.txt'), dateNow());
+      await write(getFixturePath('change.txt'), dateNow());
+      await waitFor([spy]);
+
+      await delay(300);
+      spy.should.have.been.calledWith(EV.CHANGE, changeFile);
+      spy.should.not.have.been.calledWith(EV.ADD);
+      if (!macosFswatch) spy.should.have.been.calledOnce;
+    });
+    it('should watch paths that were unwatched and added again', async () => {
+      const spy = sinon.spy();
+      const watchPaths = [getFixturePath('change.txt')];
+      const watcher = chokidar_watch(watchPaths, options);
+      await waitForWatcher(watcher);
+
+      await delay();
+      watcher.unwatch(getFixturePath('change.txt'));
+
+      await delay();
+      watcher.on(EV.ALL, spy).add(getFixturePath('change.txt'));
+
+      await delay();
+      await write(getFixturePath('change.txt'), dateNow());
+      await waitFor([spy]);
+      spy.should.have.been.calledWith(EV.CHANGE, getFixturePath('change.txt'));
+      if (!macosFswatch) spy.should.have.been.calledOnce;
+    });
+    it('should unwatch paths that are relative to options.cwd', async () => {
+      options.cwd = currentDir;
+      const watcher = chokidar_watch('.', options);
+      const spy = await aspy(watcher, EV.ALL);
+      watcher.unwatch(['subdir', getFixturePath('unlink.txt')]);
+
+      await delay();
+      await fs_unlink(getFixturePath('unlink.txt'));
+      await write(getFixturePath('subdir/add.txt'), dateNow());
+      await write(getFixturePath('change.txt'), dateNow());
+      await waitFor([spy]);
+
+      await delay(300);
+      spy.should.have.been.calledWith(EV.CHANGE, 'change.txt');
+      spy.should.not.have.been.calledWith(EV.ADD);
+      spy.should.not.have.been.calledWith(EV.UNLINK);
+      if (!macosFswatch) spy.should.have.been.calledOnce;
+    });
+  });
+  describe('env variable option override', () => {
+    describe('CHOKIDAR_USEPOLLING', () => {
+      afterEach(() => {
+        delete process.env.CHOKIDAR_USEPOLLING;
+      });
+
+      it('should make options.usePolling `true` when CHOKIDAR_USEPOLLING is set to true', async () => {
+        options.usePolling = false;
+        process.env.CHOKIDAR_USEPOLLING = 'true';
+        const watcher = chokidar_watch(currentDir, options);
+        await waitForWatcher(watcher);
+        watcher.options.usePolling.should.be.true;
+      });
+
+      it('should make options.usePolling `true` when CHOKIDAR_USEPOLLING is set to 1', async () => {
+        options.usePolling = false;
+        process.env.CHOKIDAR_USEPOLLING = '1';
+
+        const watcher = chokidar_watch(currentDir, options);
+        await waitForWatcher(watcher);
+        watcher.options.usePolling.should.be.true;
+      });
+
+      it('should make options.usePolling `false` when CHOKIDAR_USEPOLLING is set to false', async () => {
+        options.usePolling = true;
+        process.env.CHOKIDAR_USEPOLLING = 'false';
+
+        const watcher = chokidar_watch(currentDir, options);
+        await waitForWatcher(watcher);
+        watcher.options.usePolling.should.be.false;
+      });
+
+      it('should make options.usePolling `false` when CHOKIDAR_USEPOLLING is set to 0', async () => {
+        options.usePolling = true;
+        process.env.CHOKIDAR_USEPOLLING = 'false';
+
+        const watcher = chokidar_watch(currentDir, options);
+        await waitForWatcher(watcher);
+        watcher.options.usePolling.should.be.false;
+      });
+
+      it('should not attenuate options.usePolling when CHOKIDAR_USEPOLLING is set to an arbitrary value', async () => {
+        options.usePolling = true;
+        process.env.CHOKIDAR_USEPOLLING = 'foo';
+
+        const watcher = chokidar_watch(currentDir, options);
+        await waitForWatcher(watcher);
+        watcher.options.usePolling.should.be.true;
+      });
+    });
+    if (options && options.usePolling) {
+      describe('CHOKIDAR_INTERVAL', () => {
+        afterEach(() => {
+          delete process.env.CHOKIDAR_INTERVAL;
+        });
+        it('should make options.interval = CHOKIDAR_INTERVAL when it is set', async () => {
+          options.interval = 100;
+          process.env.CHOKIDAR_INTERVAL = '1500';
+
+          const watcher = chokidar_watch(currentDir, options);
+          await waitForWatcher(watcher);
+          watcher.options.interval.should.be.equal(1500);
+        });
+      });
+    }
+  });
+  describe('reproduction of bug in issue #1040', () => {
+    it('should detect change on symlink folders when consolidateThreshhold is reach', async () => {
+      const id = subdirId.toString();
+
+      const fixturesPathRel = sysPath.join(FIXTURES_PATH_REL, id, 'test-case-1040');
+      const linkPath = sysPath.join(fixturesPathRel, 'symlinkFolder');
+      const packagesPath = sysPath.join(fixturesPathRel, 'packages');
+      await fs_mkdir(fixturesPathRel);
+      await fs_mkdir(linkPath);
+      await fs_mkdir(packagesPath);
+
+      // Init chokidar
+      const watcher = chokidar.watch([]);
+
+      // Add more than 10 folders to cap consolidateThreshhold
+      for (let i = 0; i < 20; i += 1) {
+        const folderPath = sysPath.join(packagesPath, `folder${i}`);
+        await fs_mkdir(folderPath);
+        const filePath = sysPath.join(folderPath, `file${i}.js`);
+        await write(sysPath.resolve(filePath), 'file content');
+        const symlinkPath = sysPath.join(linkPath, `folder${i}`);
+        await fs_symlink(sysPath.resolve(folderPath), symlinkPath, isWindows ? 'dir' : null);
+        watcher.add(sysPath.resolve(sysPath.join(symlinkPath, `file${i}.js`)));
+      }
+
+      // Wait to be sure that we have no other event than the update file
+      await delay(300);
+
+      const eventsWaiter = waitForEvents(watcher, 1);
+
+      // Update a random generated file to fire an event
+      const randomFilePath = sysPath.join(fixturesPathRel, 'packages', 'folder17', 'file17.js');
+      await write(sysPath.resolve(randomFilePath), 'file content changer zeri ezhriez');
+
+      // Wait chokidar watch
+      await delay(300);
+
+      const events = await eventsWaiter;
+
+      expect(events.length).to.equal(1);
+    });
+  });
+  describe('reproduction of bug in issue #1024', () => {
+    it('should detect changes to folders, even if they were deleted before', async () => {
+      const id = subdirId.toString();
+      const relativeWatcherDir = sysPath.join(FIXTURES_PATH_REL, id, 'test');
+      const watcher = chokidar.watch(relativeWatcherDir, {
+        persistent: true,
+      });
+      try {
+        const eventsWaiter = waitForEvents(watcher, 5);
+        const testSubDir = sysPath.join(relativeWatcherDir, 'dir');
+        const testSubDirFile = sysPath.join(relativeWatcherDir, 'dir', 'file');
+
+        // Command sequence from https://github.com/paulmillr/chokidar/issues/1042.
+        await delay();
+        await fs_mkdir(relativeWatcherDir);
+        await fs_mkdir(testSubDir);
+        // The following delay is essential otherwise the call of mkdir and rmdir will be equalize
+        await delay(300);
+        await fs_rmdir(testSubDir);
+        // The following delay is essential otherwise the call of rmdir and mkdir will be equalize
+        await delay(300);
+        await fs_mkdir(testSubDir);
+        await delay(300);
+        await write(testSubDirFile, '');
+        await delay(300);
+
+        const events = await eventsWaiter;
+
+        chai.assert.deepStrictEqual(events, [
+          `[ALL] addDir: ${sysPath.join('test-fixtures', id, 'test')}`,
+          `[ALL] addDir: ${sysPath.join('test-fixtures', id, 'test', 'dir')}`,
+          `[ALL] unlinkDir: ${sysPath.join('test-fixtures', id, 'test', 'dir')}`,
+          `[ALL] addDir: ${sysPath.join('test-fixtures', id, 'test', 'dir')}`,
+          `[ALL] add: ${sysPath.join('test-fixtures', id, 'test', 'dir', 'file')}`,
+        ]);
+      } finally {
+        watcher.close();
+      }
+    });
+
+    it('should detect changes to symlink folders, even if they were deleted before', async () => {
+      const id = subdirId.toString();
+      const relativeWatcherDir = sysPath.join(FIXTURES_PATH_REL, id, 'test');
+      const linkedRelativeWatcherDir = sysPath.join(FIXTURES_PATH_REL, id, 'test-link');
+      await fs_symlink(
+        sysPath.resolve(relativeWatcherDir),
+        linkedRelativeWatcherDir,
+        isWindows ? 'dir' : null
+      );
+      await delay();
+      const watcher = chokidar.watch(linkedRelativeWatcherDir, {
+        persistent: true,
+      });
+      try {
+        const eventsWaiter = waitForEvents(watcher, 5);
+        const testSubDir = sysPath.join(relativeWatcherDir, 'dir');
+        const testSubDirFile = sysPath.join(relativeWatcherDir, 'dir', 'file');
+
+        // Command sequence from https://github.com/paulmillr/chokidar/issues/1042.
+        await delay();
+        await fs_mkdir(relativeWatcherDir);
+        await fs_mkdir(testSubDir);
+        // The following delay is essential otherwise the call of mkdir and rmdir will be equalize
+        await delay(300);
+        await fs_rmdir(testSubDir);
+        // The following delay is essential otherwise the call of rmdir and mkdir will be equalize
+        await delay(300);
+        await fs_mkdir(testSubDir);
+        await delay(300);
+        await write(testSubDirFile, '');
+        await delay(300);
+
+        const events = await eventsWaiter;
+
+        chai.assert.deepStrictEqual(events, [
+          `[ALL] addDir: ${sysPath.join('test-fixtures', id, 'test-link')}`,
+          `[ALL] addDir: ${sysPath.join('test-fixtures', id, 'test-link', 'dir')}`,
+          `[ALL] unlinkDir: ${sysPath.join('test-fixtures', id, 'test-link', 'dir')}`,
+          `[ALL] addDir: ${sysPath.join('test-fixtures', id, 'test-link', 'dir')}`,
+          `[ALL] add: ${sysPath.join('test-fixtures', id, 'test-link', 'dir', 'file')}`,
+        ]);
+      } finally {
+        watcher.close();
+      }
+    });
+  });
+
+  describe('close', () => {
+    it('should ignore further events on close', async () => {
+      const spy = sinon.spy();
+      const watcher = chokidar_watch(currentDir, options);
+      await waitForWatcher(watcher);
+
+      watcher.on(EV.ALL, spy);
+      await watcher.close();
+
+      await write(getFixturePath('add.txt'), dateNow());
+      await write(getFixturePath('add.txt'), 'hello');
+      await delay(300);
+      await fs_unlink(getFixturePath('add.txt'));
+
+      spy.should.not.have.been.called;
+    });
+    it('should not ignore further events on close with existing watchers', async () => {
+      const spy = sinon.spy();
+      const watcher1 = chokidar_watch(currentDir);
+      const watcher2 = chokidar_watch(currentDir);
+      await Promise.all([waitForWatcher(watcher1), waitForWatcher(watcher2)]);
+
+      // The EV_ADD event should be called on the second watcher even if the first watcher is closed
+      watcher2.on(EV.ADD, spy);
+      await watcher1.close();
+
+      await write(getFixturePath('add.txt'), 'hello');
+      // Ensures EV_ADD is called. Immediately removing the file causes it to be skipped
+      await delay(200);
+      await fs_unlink(getFixturePath('add.txt'));
+
+      spy.should.have.been.calledWith(sinon.match('add.txt'));
+    });
+    it('should not prevent the process from exiting', async () => {
+      const scriptFile = getFixturePath('script.js');
+      const chokidarPath = pathToFileURL(sysPath.join(__dirname, 'lib/v4.js')).href.replace(
+        /\\/g,
+        '\\\\'
+      );
+      const scriptContent = `
+      (async () => {
+        const chokidar = await import("${chokidarPath}");
+        const watcher = chokidar.watch("${scriptFile.replace(/\\/g, '\\\\')}");
+        watcher.on("ready", () => {
+          watcher.close();
+          process.stdout.write("closed");
+        });
+      })();`;
+      await write(scriptFile, scriptContent);
+      const obj = await exec(`node ${scriptFile}`);
+      const { stdout } = obj;
+      expect(stdout.toString()).to.equal('closed');
+    });
+    it('should always return the same promise', async () => {
+      const watcher = chokidar_watch(currentDir, options);
+      const closePromise = watcher.close();
+      expect(closePromise).to.be.a('promise');
+      expect(watcher.close()).to.be.equal(closePromise);
+      await closePromise;
+    });
+  });
+};
+
+describe('chokidar', async () => {
+  before(async () => {
+    await rimraf(FIXTURES_PATH);
+    const _content = fs.readFileSync(__filename, 'utf-8');
+    const _only = _content.match(/\sit\.only\(/g);
+    const itCount = (_only && _only.length) || _content.match(/\sit\(/g).length;
+    const testCount = itCount * 3;
+    fs.mkdirSync(currentDir, PERM_ARR);
+    while (subdirId++ < testCount) {
+      currentDir = getFixturePath('');
+      fs.mkdirSync(currentDir, PERM_ARR);
+      fs.writeFileSync(sysPath.join(currentDir, 'change.txt'), 'b');
+      fs.writeFileSync(sysPath.join(currentDir, 'unlink.txt'), 'b');
+    }
+    subdirId = 0;
+  });
+
+  after(async () => {
+    await rimraf(FIXTURES_PATH);
+  });
+
+  beforeEach(() => {
+    subdirId++;
+    currentDir = getFixturePath('');
+  });
+
+  afterEach(async () => {
+    let watcher;
+    while ((watcher = allWatchers.pop())) {
+      await watcher.close();
+    }
+  });
+
+  it('should expose public API methods', () => {
+    chokidar.FSWatcher.should.be.a('function');
+    chokidar.watch.should.be.a('function');
+  });
+
+  if (!isIBMi) {
+    describe('fs.watch (non-polling)', runTests.bind(this, { usePolling: false }));
+  }
+  describe('fs.watchFile (polling)', runTests.bind(this, { usePolling: true, interval: 10 }));
+});
diff --git a/tsconfig.json b/tsconfig.json
index 1bebc3f6..42041e71 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,6 +12,6 @@
     "esModuleInterop": true,
     "baseUrl": ".",
   },
-  "include": ["src", "types"],
+  "include": ["src/v4.ts"],
   "exclude": ["node_modules", "lib"]
 }