Comparing version 3.6.0 to 4.0.0
1682
index.js
@@ -1,280 +0,227 @@ | ||
'use strict'; | ||
const { EventEmitter } = require('events'); | ||
const fs = require('fs'); | ||
const sysPath = require('path'); | ||
const { promisify } = require('util'); | ||
const readdirp = require('readdirp'); | ||
const anymatch = require('anymatch').default; | ||
const globParent = require('glob-parent'); | ||
const isGlob = require('is-glob'); | ||
const braces = require('braces'); | ||
const normalizePath = require('normalize-path'); | ||
const NodeFsHandler = require('./lib/nodefs-handler'); | ||
const FsEventsHandler = require('./lib/fsevents-handler'); | ||
const { | ||
EV_ALL, | ||
EV_READY, | ||
EV_ADD, | ||
EV_CHANGE, | ||
EV_UNLINK, | ||
EV_ADD_DIR, | ||
EV_UNLINK_DIR, | ||
EV_RAW, | ||
EV_ERROR, | ||
STR_CLOSE, | ||
STR_END, | ||
BACK_SLASH_RE, | ||
DOUBLE_SLASH_RE, | ||
SLASH_OR_BACK_SLASH_RE, | ||
DOT_RE, | ||
REPLACER_RE, | ||
SLASH, | ||
SLASH_SLASH, | ||
BRACE_START, | ||
BANG, | ||
ONE_DOT, | ||
TWO_DOTS, | ||
GLOBSTAR, | ||
SLASH_GLOBSTAR, | ||
ANYMATCH_OPTS, | ||
STRING_TYPE, | ||
FUNCTION_TYPE, | ||
EMPTY_STR, | ||
EMPTY_FN, | ||
isWindows, | ||
isMacos, | ||
isIBMi | ||
} = require('./lib/constants'); | ||
const stat = promisify(fs.stat); | ||
const readdir = promisify(fs.readdir); | ||
/** | ||
* @typedef {String} Path | ||
* @typedef {'all'|'add'|'addDir'|'change'|'unlink'|'unlinkDir'|'raw'|'error'|'ready'} EventName | ||
* @typedef {'readdir'|'watch'|'add'|'remove'|'change'} ThrottleType | ||
*/ | ||
/** | ||
* | ||
* @typedef {Object} WatchHelpers | ||
* @property {Boolean} followSymlinks | ||
* @property {'stat'|'lstat'} statMethod | ||
* @property {Path} path | ||
* @property {Path} watchPath | ||
* @property {Function} entryPath | ||
* @property {Boolean} hasGlob | ||
* @property {Object} globFilter | ||
* @property {Function} filterPath | ||
* @property {Function} filterDir | ||
*/ | ||
const arrify = (value = []) => Array.isArray(value) ? value : [value]; | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.watch = exports.FSWatcher = exports.WatchHelper = void 0; | ||
const fs_1 = require("fs"); | ||
const promises_1 = require("fs/promises"); | ||
const events_1 = require("events"); | ||
const sysPath = require("path"); | ||
const readdirp_1 = require("readdirp"); | ||
const handler_js_1 = require("./handler.js"); | ||
const SLASH = '/'; | ||
const SLASH_SLASH = '//'; | ||
const ONE_DOT = '.'; | ||
const TWO_DOTS = '..'; | ||
const STRING_TYPE = 'string'; | ||
const BACK_SLASH_RE = /\\/g; | ||
const DOUBLE_SLASH_RE = /\/\//; | ||
const DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/; | ||
const REPLACER_RE = /^\.[/\\]/; | ||
function arrify(item) { | ||
return Array.isArray(item) ? item : [item]; | ||
} | ||
const isMatcherObject = (matcher) => typeof matcher === 'object' && matcher !== null && !(matcher instanceof RegExp); | ||
function createPattern(matcher) { | ||
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.recursive) { | ||
const relative = sysPath.relative(matcher.path, string); | ||
if (!relative) { | ||
return false; | ||
} | ||
return !relative.startsWith('..') && !sysPath.isAbsolute(relative); | ||
} | ||
return false; | ||
}; | ||
} | ||
return () => false; | ||
} | ||
function normalizePath(path) { | ||
if (typeof path !== 'string') | ||
throw new Error('string expected'); | ||
path = sysPath.normalize(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; | ||
return path; | ||
} | ||
function matchPatterns(patterns, testString, stats) { | ||
const path = normalizePath(testString); | ||
for (let index = 0; index < patterns.length; index++) { | ||
const pattern = patterns[index]; | ||
if (pattern(path, stats)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
function anymatch(matchers, testString) { | ||
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)); | ||
if (testString == null) { | ||
return (testString, stats) => { | ||
return matchPatterns(patterns, testString, stats); | ||
}; | ||
} | ||
return matchPatterns(patterns, testString); | ||
} | ||
const flatten = (list, result = []) => { | ||
list.forEach(item => { | ||
if (Array.isArray(item)) { | ||
flatten(item, result); | ||
} else { | ||
result.push(item); | ||
} | ||
}); | ||
return result; | ||
list.forEach((item) => { | ||
if (Array.isArray(item)) { | ||
flatten(item, result); | ||
} | ||
else { | ||
// @ts-ignore | ||
result.push(item); | ||
} | ||
}); | ||
return result; | ||
}; | ||
const unifyPaths = (paths_) => { | ||
/** | ||
* @type {Array<String>} | ||
*/ | ||
const paths = flatten(arrify(paths_)); | ||
if (!paths.every(p => typeof p === STRING_TYPE)) { | ||
throw new TypeError(`Non-string provided as watch path: ${paths}`); | ||
} | ||
return paths.map(normalizePathToUnix); | ||
const paths = flatten(arrify(paths_)); | ||
if (!paths.every((p) => typeof p === STRING_TYPE)) { | ||
throw new TypeError(`Non-string provided as watch path: ${paths}`); | ||
} | ||
return paths.map(normalizePathToUnix); | ||
}; | ||
// If SLASH_SLASH occurs at the beginning of path, it is not replaced | ||
// because "//StoragePC/DrivePool/Movies" is a valid network path | ||
const toUnix = (string) => { | ||
let str = string.replace(BACK_SLASH_RE, SLASH); | ||
let prepend = false; | ||
if (str.startsWith(SLASH_SLASH)) { | ||
prepend = true; | ||
} | ||
while (str.match(DOUBLE_SLASH_RE)) { | ||
str = str.replace(DOUBLE_SLASH_RE, SLASH); | ||
} | ||
if (prepend) { | ||
str = SLASH + str; | ||
} | ||
return str; | ||
let str = string.replace(BACK_SLASH_RE, SLASH); | ||
let prepend = false; | ||
if (str.startsWith(SLASH_SLASH)) { | ||
prepend = true; | ||
} | ||
while (str.match(DOUBLE_SLASH_RE)) { | ||
str = str.replace(DOUBLE_SLASH_RE, SLASH); | ||
} | ||
if (prepend) { | ||
str = SLASH + str; | ||
} | ||
return str; | ||
}; | ||
// Our version of upath.normalize | ||
// TODO: this is not equal to path-normalize module - investigate why | ||
const normalizePathToUnix = (path) => toUnix(sysPath.normalize(toUnix(path))); | ||
const normalizeIgnored = (cwd = EMPTY_STR) => (path) => { | ||
if (typeof path !== STRING_TYPE) return path; | ||
return normalizePathToUnix(sysPath.isAbsolute(path) ? path : sysPath.join(cwd, path)); | ||
// TODO: refactor | ||
const normalizeIgnored = (cwd = '') => (path) => { | ||
if (typeof path === 'string') { | ||
return normalizePathToUnix(sysPath.isAbsolute(path) ? path : sysPath.join(cwd, path)); | ||
} | ||
else { | ||
return path; | ||
} | ||
}; | ||
const getAbsolutePath = (path, cwd) => { | ||
if (sysPath.isAbsolute(path)) { | ||
return path; | ||
} | ||
if (path.startsWith(BANG)) { | ||
return BANG + sysPath.join(cwd, path.slice(1)); | ||
} | ||
return sysPath.join(cwd, path); | ||
if (sysPath.isAbsolute(path)) { | ||
return path; | ||
} | ||
return sysPath.join(cwd, path); | ||
}; | ||
const undef = (opts, key) => opts[key] === undefined; | ||
const EMPTY_SET = Object.freeze(new Set()); | ||
/** | ||
* Directory entry. | ||
* @property {Path} path | ||
* @property {Set<Path>} items | ||
*/ | ||
class DirEntry { | ||
/** | ||
* @param {Path} dir | ||
* @param {Function} removeWatcher | ||
*/ | ||
constructor(dir, removeWatcher) { | ||
this.path = dir; | ||
this._removeWatcher = removeWatcher; | ||
/** @type {Set<Path>} */ | ||
this.items = new Set(); | ||
} | ||
add(item) { | ||
const {items} = this; | ||
if (!items) return; | ||
if (item !== ONE_DOT && item !== TWO_DOTS) items.add(item); | ||
} | ||
async remove(item) { | ||
const {items} = this; | ||
if (!items) return; | ||
items.delete(item); | ||
if (items.size > 0) return; | ||
const dir = this.path; | ||
try { | ||
await readdir(dir); | ||
} catch (err) { | ||
if (this._removeWatcher) { | ||
this._removeWatcher(sysPath.dirname(dir), sysPath.basename(dir)); | ||
} | ||
constructor(dir, removeWatcher) { | ||
this.path = dir; | ||
this._removeWatcher = removeWatcher; | ||
/** @type {Set<Path>} */ | ||
this.items = new Set(); | ||
} | ||
} | ||
has(item) { | ||
const {items} = this; | ||
if (!items) return; | ||
return items.has(item); | ||
} | ||
/** | ||
* @returns {Array<String>} | ||
*/ | ||
getChildren() { | ||
const {items} = this; | ||
if (!items) return; | ||
return [...items.values()]; | ||
} | ||
dispose() { | ||
this.items.clear(); | ||
delete this.path; | ||
delete this._removeWatcher; | ||
delete this.items; | ||
Object.freeze(this); | ||
} | ||
add(item) { | ||
const { items } = this; | ||
if (!items) | ||
return; | ||
if (item !== ONE_DOT && item !== TWO_DOTS) | ||
items.add(item); | ||
} | ||
async remove(item) { | ||
const { items } = this; | ||
if (!items) | ||
return; | ||
items.delete(item); | ||
if (items.size > 0) | ||
return; | ||
const dir = this.path; | ||
try { | ||
await (0, promises_1.readdir)(dir); | ||
} | ||
catch (err) { | ||
if (this._removeWatcher) { | ||
this._removeWatcher(sysPath.dirname(dir), sysPath.basename(dir)); | ||
} | ||
} | ||
} | ||
has(item) { | ||
const { items } = this; | ||
if (!items) | ||
return; | ||
return items.has(item); | ||
} | ||
getChildren() { | ||
const { items } = this; | ||
if (!items) | ||
return []; | ||
return [...items.values()]; | ||
} | ||
dispose() { | ||
this.items.clear(); | ||
this.path = ''; | ||
this._removeWatcher = handler_js_1.EMPTY_FN; | ||
this.items = EMPTY_SET; | ||
Object.freeze(this); | ||
} | ||
} | ||
const STAT_METHOD_F = 'stat'; | ||
const STAT_METHOD_L = 'lstat'; | ||
class WatchHelper { | ||
constructor(path, watchPath, follow, fsw) { | ||
this.fsw = fsw; | ||
this.path = path = path.replace(REPLACER_RE, EMPTY_STR); | ||
this.watchPath = watchPath; | ||
this.fullWatchPath = sysPath.resolve(watchPath); | ||
this.hasGlob = watchPath !== path; | ||
/** @type {object|boolean} */ | ||
if (path === EMPTY_STR) this.hasGlob = false; | ||
this.globSymlink = this.hasGlob && follow ? undefined : false; | ||
this.globFilter = this.hasGlob ? anymatch(path, undefined, ANYMATCH_OPTS) : false; | ||
this.dirParts = this.getDirParts(path); | ||
this.dirParts.forEach((parts) => { | ||
if (parts.length > 1) parts.pop(); | ||
}); | ||
this.followSymlinks = follow; | ||
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L; | ||
} | ||
checkGlobSymlink(entry) { | ||
// only need to resolve once | ||
// first entry should always have entry.parentDir === EMPTY_STR | ||
if (this.globSymlink === undefined) { | ||
this.globSymlink = entry.fullParentDir === this.fullWatchPath ? | ||
false : {realPath: entry.fullParentDir, linkPath: this.fullWatchPath}; | ||
constructor(path, follow, fsw) { | ||
this.fsw = fsw; | ||
const watchPath = path; | ||
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(); | ||
}); | ||
this.followSymlinks = follow; | ||
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L; | ||
} | ||
if (this.globSymlink) { | ||
return entry.fullPath.replace(this.globSymlink.realPath, this.globSymlink.linkPath); | ||
entryPath(entry) { | ||
return sysPath.join(this.watchPath, sysPath.relative(this.watchPath, entry.fullPath)); | ||
} | ||
return entry.fullPath; | ||
} | ||
entryPath(entry) { | ||
return sysPath.join(this.watchPath, | ||
sysPath.relative(this.watchPath, this.checkGlobSymlink(entry)) | ||
); | ||
} | ||
filterPath(entry) { | ||
const {stats} = entry; | ||
if (stats && stats.isSymbolicLink()) return this.filterDir(entry); | ||
const resolvedPath = this.entryPath(entry); | ||
const matchesGlob = this.hasGlob && typeof this.globFilter === FUNCTION_TYPE ? | ||
this.globFilter(resolvedPath) : true; | ||
return matchesGlob && | ||
this.fsw._isntIgnored(resolvedPath, stats) && | ||
this.fsw._hasReadPermissions(stats); | ||
} | ||
getDirParts(path) { | ||
if (!this.hasGlob) return []; | ||
const parts = []; | ||
const expandedPath = path.includes(BRACE_START) ? braces.expand(path) : [path]; | ||
expandedPath.forEach((path) => { | ||
parts.push(sysPath.relative(this.watchPath, path).split(SLASH_OR_BACK_SLASH_RE)); | ||
}); | ||
return parts; | ||
} | ||
filterDir(entry) { | ||
if (this.hasGlob) { | ||
const entryParts = this.getDirParts(this.checkGlobSymlink(entry)); | ||
let globstar = false; | ||
this.unmatchedGlob = !this.dirParts.some((parts) => { | ||
return parts.every((part, i) => { | ||
if (part === GLOBSTAR) globstar = true; | ||
return globstar || !entryParts[0][i] || anymatch(part, entryParts[0][i], ANYMATCH_OPTS); | ||
}); | ||
}); | ||
filterPath(entry) { | ||
const { stats } = entry; | ||
if (stats && stats.isSymbolicLink()) | ||
return this.filterDir(entry); | ||
const resolvedPath = this.entryPath(entry); | ||
// TODO: what if stats is undefined? remove ! | ||
return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats); | ||
} | ||
return !this.unmatchedGlob && this.fsw._isntIgnored(this.entryPath(entry), entry.stats); | ||
} | ||
filterDir(entry) { | ||
return this.fsw._isntIgnored(this.entryPath(entry), entry.stats); | ||
} | ||
} | ||
exports.WatchHelper = WatchHelper; | ||
/** | ||
@@ -288,687 +235,586 @@ * Watches files & directories for changes. Emitted events: | ||
*/ | ||
class FSWatcher extends EventEmitter { | ||
// Not indenting methods for history sake; for now. | ||
constructor(_opts) { | ||
super(); | ||
const opts = {}; | ||
if (_opts) Object.assign(opts, _opts); // for frozen objects | ||
/** @type {Map<String, DirEntry>} */ | ||
this._watched = new Map(); | ||
/** @type {Map<String, Array>} */ | ||
this._closers = new Map(); | ||
/** @type {Set<String>} */ | ||
this._ignoredPaths = new Set(); | ||
/** @type {Map<ThrottleType, Map>} */ | ||
this._throttled = new Map(); | ||
/** @type {Map<Path, String|Boolean>} */ | ||
this._symlinkPaths = new Map(); | ||
this._streams = new Set(); | ||
this.closed = false; | ||
// Set up default options. | ||
if (undef(opts, 'persistent')) opts.persistent = true; | ||
if (undef(opts, 'ignoreInitial')) opts.ignoreInitial = false; | ||
if (undef(opts, 'ignorePermissionErrors')) opts.ignorePermissionErrors = false; | ||
if (undef(opts, 'interval')) opts.interval = 100; | ||
if (undef(opts, 'binaryInterval')) opts.binaryInterval = 300; | ||
if (undef(opts, 'disableGlobbing')) opts.disableGlobbing = false; | ||
opts.enableBinaryInterval = opts.binaryInterval !== opts.interval; | ||
// Enable fsevents on OS X when polling isn't explicitly enabled. | ||
if (undef(opts, 'useFsEvents')) opts.useFsEvents = !opts.usePolling; | ||
// If we can't use fsevents, ensure the options reflect it's disabled. | ||
const canUseFsEvents = FsEventsHandler.canUse(); | ||
if (!canUseFsEvents) opts.useFsEvents = false; | ||
// Use polling on Mac if not using fsevents. | ||
// Other platforms use non-polling fs_watch. | ||
if (undef(opts, 'usePolling') && !opts.useFsEvents) { | ||
opts.usePolling = isMacos; | ||
} | ||
// Always default to polling on IBM i because fs.watch() is not available on IBM i. | ||
if(isIBMi) { | ||
opts.usePolling = true; | ||
} | ||
// Global override (useful for end-developers that 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; | ||
class FSWatcher extends events_1.EventEmitter { | ||
// Not indenting methods for history sake; for now. | ||
constructor(_opts = {}) { | ||
super(); | ||
this.closed = false; | ||
this._closers = new Map(); | ||
this._ignoredPaths = new Set(); | ||
this._throttled = new Map(); | ||
this._streams = new Set(); | ||
this._symlinkPaths = new Map(); | ||
this._watched = new Map(); | ||
this._pendingWrites = new Map(); | ||
this._pendingUnlinks = new Map(); | ||
this._readyCount = 0; | ||
this._readyEmitted = false; | ||
const awf = _opts.awaitWriteFinish; | ||
const DEF_AWF = { stabilityThreshold: 2000, pollInterval: 100 }; | ||
const opts = { | ||
// 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: _opts.ignored ? arrify(_opts.ignored) : arrify([]), | ||
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 (handler_js_1.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 is done to emit ready only once, but each 'add' will increase that? | ||
let readyCalls = 0; | ||
this._emitReady = () => { | ||
readyCalls++; | ||
if (readyCalls >= this._readyCount) { | ||
this._emitReady = handler_js_1.EMPTY_FN; | ||
this._readyEmitted = true; | ||
// use process.nextTick to allow time for listener to be bound | ||
process.nextTick(() => this.emit(handler_js_1.EVENTS.READY)); | ||
} | ||
}; | ||
this._emitRaw = (...args) => this.emit(handler_js_1.EVENTS.RAW, ...args); | ||
this._boundRemove = this._remove.bind(this); | ||
this.options = opts; | ||
this._nodeFsHandler = new handler_js_1.NodeFsHandler(this); | ||
// You’re frozen when your heart’s not open. | ||
Object.freeze(opts); | ||
} | ||
} | ||
const envInterval = process.env.CHOKIDAR_INTERVAL; | ||
if (envInterval) { | ||
opts.interval = Number.parseInt(envInterval, 10); | ||
} | ||
// Editor atomic write normalization enabled by default with fs.watch | ||
if (undef(opts, 'atomic')) opts.atomic = !opts.usePolling && !opts.useFsEvents; | ||
if (opts.atomic) this._pendingUnlinks = new Map(); | ||
if (undef(opts, 'followSymlinks')) opts.followSymlinks = true; | ||
if (undef(opts, 'awaitWriteFinish')) opts.awaitWriteFinish = false; | ||
if (opts.awaitWriteFinish === true) opts.awaitWriteFinish = {}; | ||
const awf = opts.awaitWriteFinish; | ||
if (awf) { | ||
if (!awf.stabilityThreshold) awf.stabilityThreshold = 2000; | ||
if (!awf.pollInterval) awf.pollInterval = 100; | ||
this._pendingWrites = new Map(); | ||
} | ||
if (opts.ignored) opts.ignored = arrify(opts.ignored); | ||
let readyCalls = 0; | ||
this._emitReady = () => { | ||
readyCalls++; | ||
if (readyCalls >= this._readyCount) { | ||
this._emitReady = EMPTY_FN; | ||
this._readyEmitted = true; | ||
// use process.nextTick to allow time for listener to be bound | ||
process.nextTick(() => this.emit(EV_READY)); | ||
_addIgnoredPath(matcher) { | ||
if (isMatcherObject(matcher)) { | ||
// return early if we already have a deeply equal matcher object | ||
for (const ignored of this._ignoredPaths) { | ||
if (isMatcherObject(ignored) && | ||
ignored.path === matcher.path && | ||
ignored.recursive === matcher.recursive) { | ||
return; | ||
} | ||
} | ||
} | ||
this._ignoredPaths.add(matcher); | ||
} | ||
}; | ||
this._emitRaw = (...args) => this.emit(EV_RAW, ...args); | ||
this._readyEmitted = false; | ||
this.options = opts; | ||
// Initialize with proper watcher. | ||
if (opts.useFsEvents) { | ||
this._fsEventsHandler = new FsEventsHandler(this); | ||
} else { | ||
this._nodeFsHandler = new NodeFsHandler(this); | ||
} | ||
// You’re frozen when your heart’s not open. | ||
Object.freeze(opts); | ||
} | ||
// Public methods | ||
/** | ||
* 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_, _origAdd, _internal) { | ||
const {cwd, disableGlobbing} = this.options; | ||
this.closed = false; | ||
let paths = unifyPaths(paths_); | ||
if (cwd) { | ||
paths = paths.map((path) => { | ||
const absPath = getAbsolutePath(path, cwd); | ||
// Check `path` instead of `absPath` because the cwd portion can't be a glob | ||
if (disableGlobbing || !isGlob(path)) { | ||
return absPath; | ||
} | ||
return normalizePath(absPath); | ||
}); | ||
} | ||
// set aside negated glob strings | ||
paths = paths.filter((path) => { | ||
if (path.startsWith(BANG)) { | ||
this._ignoredPaths.add(path.slice(1)); | ||
return false; | ||
_removeIgnoredPath(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); | ||
} | ||
} | ||
} | ||
} | ||
// if a path is being added that was previously ignored, stop ignoring it | ||
this._ignoredPaths.delete(path); | ||
this._ignoredPaths.delete(path + SLASH_GLOBSTAR); | ||
// reset the cached userIgnored anymatch fn | ||
// to make ignoredPaths changes effective | ||
this._userIgnored = undefined; | ||
return true; | ||
}); | ||
if (this.options.useFsEvents && this._fsEventsHandler) { | ||
if (!this._readyCount) this._readyCount = paths.length; | ||
if (this.options.persistent) this._readyCount += paths.length; | ||
paths.forEach((path) => this._fsEventsHandler._addToFsEvents(path)); | ||
} else { | ||
if (!this._readyCount) this._readyCount = 0; | ||
this._readyCount += paths.length; | ||
Promise.all( | ||
paths.map(async path => { | ||
const res = await this._nodeFsHandler._addToNodeFs(path, !_internal, 0, 0, _origAdd); | ||
if (res) this._emitReady(); | ||
return res; | ||
}) | ||
).then(results => { | ||
if (this.closed) return; | ||
results.filter(item => item).forEach(item => { | ||
this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item)); | ||
}); | ||
}); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Close watchers or start ignoring events from specified paths. | ||
* @param {Path|Array<Path>} paths_ - string or array of strings, file/directory paths and/or globs | ||
* @returns {FSWatcher} for chaining | ||
*/ | ||
unwatch(paths_) { | ||
if (this.closed) return this; | ||
const paths = unifyPaths(paths_); | ||
const {cwd} = this.options; | ||
paths.forEach((path) => { | ||
// convert to absolute path unless relative path already matches | ||
if (!sysPath.isAbsolute(path) && !this._closers.has(path)) { | ||
if (cwd) path = sysPath.join(cwd, path); | ||
path = sysPath.resolve(path); | ||
// Public methods | ||
/** | ||
* Adds paths to be watched on an existing FSWatcher instance. | ||
* @param paths_ file or file list. Other arguments are unused | ||
*/ | ||
add(paths_, _origAdd, _internal) { | ||
const { cwd } = this.options; | ||
this.closed = false; | ||
let paths = unifyPaths(paths_); | ||
if (cwd) { | ||
paths = paths.map((path) => { | ||
const absPath = getAbsolutePath(path, cwd); | ||
// Check `path` instead of `absPath` because the cwd portion can't be a glob | ||
return absPath; | ||
}); | ||
} | ||
paths.forEach((path) => { | ||
this._removeIgnoredPath(path); | ||
}); | ||
this._userIgnored = undefined; | ||
if (!this._readyCount) | ||
this._readyCount = 0; | ||
this._readyCount += paths.length; | ||
Promise.all(paths.map(async (path) => { | ||
const res = await this._nodeFsHandler._addToNodeFs(path, !_internal, undefined, 0, _origAdd); | ||
if (res) | ||
this._emitReady(); | ||
return res; | ||
})).then((results) => { | ||
if (this.closed) | ||
return; | ||
results.forEach((item) => { | ||
if (item) | ||
this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item)); | ||
}); | ||
}); | ||
return this; | ||
} | ||
this._closePath(path); | ||
this._ignoredPaths.add(path); | ||
if (this._watched.has(path)) { | ||
this._ignoredPaths.add(path + SLASH_GLOBSTAR); | ||
/** | ||
* Close watchers or start ignoring events from specified paths. | ||
*/ | ||
unwatch(paths_) { | ||
if (this.closed) | ||
return this; | ||
const paths = unifyPaths(paths_); | ||
const { cwd } = this.options; | ||
paths.forEach((path) => { | ||
// convert to absolute path unless relative path already matches | ||
if (!sysPath.isAbsolute(path) && !this._closers.has(path)) { | ||
if (cwd) | ||
path = sysPath.join(cwd, path); | ||
path = sysPath.resolve(path); | ||
} | ||
this._closePath(path); | ||
this._addIgnoredPath(path); | ||
if (this._watched.has(path)) { | ||
this._addIgnoredPath({ | ||
path, | ||
recursive: true, | ||
}); | ||
} | ||
// reset the cached userIgnored anymatch fn | ||
// to make ignoredPaths changes effective | ||
this._userIgnored = undefined; | ||
}); | ||
return this; | ||
} | ||
// reset the cached userIgnored anymatch fn | ||
// to make ignoredPaths changes effective | ||
this._userIgnored = undefined; | ||
}); | ||
return this; | ||
} | ||
/** | ||
* Close watchers and remove all listeners from watched paths. | ||
* @returns {Promise<void>}. | ||
*/ | ||
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._streams.forEach(stream => stream.destroy()); | ||
this._userIgnored = undefined; | ||
this._readyCount = 0; | ||
this._readyEmitted = false; | ||
this._watched.forEach(dirent => dirent.dispose()); | ||
['closers', 'watched', 'streams', 'symlinkPaths', 'throttled'].forEach(key => { | ||
this[`_${key}`].clear(); | ||
}); | ||
this._closePromise = closers.length ? Promise.all(closers).then(() => undefined) : Promise.resolve(); | ||
return this._closePromise; | ||
} | ||
/** | ||
* Expose list of watched paths | ||
* @returns {Object} for chaining | ||
*/ | ||
getWatched() { | ||
const watchList = {}; | ||
this._watched.forEach((entry, dir) => { | ||
const key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir; | ||
watchList[key || ONE_DOT] = entry.getChildren().sort(); | ||
}); | ||
return watchList; | ||
} | ||
emitWithAll(event, args) { | ||
this.emit(...args); | ||
if (event !== EV_ERROR) this.emit(EV_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 | ||
* @param {*=} val1 arguments to be passed with event | ||
* @param {*=} val2 | ||
* @param {*=} val3 | ||
* @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag | ||
*/ | ||
async _emit(event, path, val1, val2, val3) { | ||
if (this.closed) return; | ||
const opts = this.options; | ||
if (isWindows) path = sysPath.normalize(path); | ||
if (opts.cwd) path = sysPath.relative(opts.cwd, path); | ||
/** @type Array<any> */ | ||
const args = [event, path]; | ||
if (val3 !== undefined) args.push(val1, val2, val3); | ||
else if (val2 !== undefined) args.push(val1, val2); | ||
else if (val1 !== undefined) args.push(val1); | ||
const awf = opts.awaitWriteFinish; | ||
let pw; | ||
if (awf && (pw = this._pendingWrites.get(path))) { | ||
pw.lastChange = new Date(); | ||
return this; | ||
} | ||
if (opts.atomic) { | ||
if (event === EV_UNLINK) { | ||
this._pendingUnlinks.set(path, args); | ||
setTimeout(() => { | ||
this._pendingUnlinks.forEach((entry, path) => { | ||
this.emit(...entry); | ||
this.emit(EV_ALL, ...entry); | ||
this._pendingUnlinks.delete(path); | ||
/** | ||
* Close watchers and remove all listeners from watched paths. | ||
* @returns {Promise<void>}. | ||
*/ | ||
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._streams.forEach((stream) => stream.destroy()); | ||
this._userIgnored = undefined; | ||
this._readyCount = 0; | ||
this._readyEmitted = false; | ||
this._watched.forEach((dirent) => dirent.dispose()); | ||
this._closers.clear(); | ||
this._watched.clear(); | ||
this._streams.clear(); | ||
this._symlinkPaths.clear(); | ||
this._throttled.clear(); | ||
this._closePromise = closers.length | ||
? Promise.all(closers).then(() => undefined) | ||
: Promise.resolve(); | ||
return this._closePromise; | ||
} | ||
/** | ||
* Expose list of watched paths | ||
* @returns {Object} for chaining | ||
*/ | ||
getWatched() { | ||
const watchList = {}; | ||
this._watched.forEach((entry, dir) => { | ||
const key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir; | ||
const index = key || ONE_DOT; | ||
// @ts-ignore | ||
watchList[index] = entry.getChildren().sort(); | ||
}); | ||
}, typeof opts.atomic === 'number' ? opts.atomic : 100); | ||
return this; | ||
return watchList; | ||
} | ||
if (event === EV_ADD && this._pendingUnlinks.has(path)) { | ||
event = args[0] = EV_CHANGE; | ||
this._pendingUnlinks.delete(path); | ||
emitWithAll(event, args) { | ||
this.emit(...args); | ||
if (event !== handler_js_1.EVENTS.ERROR) | ||
this.emit(handler_js_1.EVENTS.ALL, ...args); | ||
} | ||
} | ||
if (awf && (event === EV_ADD || event === EV_CHANGE) && this._readyEmitted) { | ||
const awfEmit = (err, stats) => { | ||
if (err) { | ||
event = args[0] = EV_ERROR; | ||
args[1] = err; | ||
this.emitWithAll(event, args); | ||
} else if (stats) { | ||
// if stats doesn't exist the file must have been deleted | ||
if (args.length > 2) { | ||
args[2] = stats; | ||
} else { | ||
args.push(stats); | ||
// Common helpers | ||
// -------------- | ||
/** | ||
* Normalize and emit events. | ||
* Calling _emit DOES NOT MEAN emit() would be called! | ||
* @param event Type of event | ||
* @param path File or directory path | ||
* @param stats arguments to be passed with event | ||
* @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag | ||
*/ | ||
async _emit(event, path, stats) { | ||
if (this.closed) | ||
return; | ||
const opts = this.options; | ||
if (handler_js_1.isWindows) | ||
path = sysPath.normalize(path); | ||
if (opts.cwd) | ||
path = sysPath.relative(opts.cwd, path); | ||
/** @type Array<any> */ | ||
const args = [event, path]; | ||
if (stats != null) | ||
args.push(stats); | ||
const awf = opts.awaitWriteFinish; | ||
let pw; | ||
if (awf && (pw = this._pendingWrites.get(path))) { | ||
pw.lastChange = new Date(); | ||
return this; | ||
} | ||
if (opts.atomic) { | ||
if (event === handler_js_1.EVENTS.UNLINK) { | ||
this._pendingUnlinks.set(path, args); | ||
setTimeout(() => { | ||
this._pendingUnlinks.forEach((entry, path) => { | ||
this.emit(...entry); | ||
this.emit(handler_js_1.EVENTS.ALL, ...entry); | ||
this._pendingUnlinks.delete(path); | ||
}); | ||
}, typeof opts.atomic === 'number' ? opts.atomic : 100); | ||
return this; | ||
} | ||
if (event === handler_js_1.EVENTS.ADD && this._pendingUnlinks.has(path)) { | ||
event = args[0] = handler_js_1.EVENTS.CHANGE; | ||
this._pendingUnlinks.delete(path); | ||
} | ||
} | ||
if (awf && (event === handler_js_1.EVENTS.ADD || event === handler_js_1.EVENTS.CHANGE) && this._readyEmitted) { | ||
const awfEmit = (err, stats) => { | ||
if (err) { | ||
event = args[0] = handler_js_1.EVENTS.ERROR; | ||
// @ts-ignore | ||
args[1] = err; | ||
this.emitWithAll(event, args); | ||
} | ||
else if (stats) { | ||
// if stats doesn't exist the file must have been deleted | ||
if (args.length > 2) { | ||
args[2] = stats; | ||
} | ||
else { | ||
args.push(stats); | ||
} | ||
this.emitWithAll(event, args); | ||
} | ||
}; | ||
this._awaitWriteFinish(path, awf.stabilityThreshold, event, awfEmit); | ||
return this; | ||
} | ||
if (event === handler_js_1.EVENTS.CHANGE) { | ||
const isThrottled = !this._throttle(handler_js_1.EVENTS.CHANGE, path, 50); | ||
if (isThrottled) | ||
return this; | ||
} | ||
if (opts.alwaysStat && | ||
stats === undefined && | ||
(event === handler_js_1.EVENTS.ADD || event === handler_js_1.EVENTS.ADD_DIR || event === handler_js_1.EVENTS.CHANGE)) { | ||
const fullPath = opts.cwd ? sysPath.join(opts.cwd, path) : path; | ||
let stats; | ||
try { | ||
stats = await (0, promises_1.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); | ||
} | ||
this.emitWithAll(event, args); | ||
} | ||
}; | ||
this._awaitWriteFinish(path, awf.stabilityThreshold, event, awfEmit); | ||
return this; | ||
} | ||
if (event === EV_CHANGE) { | ||
const isThrottled = !this._throttle(EV_CHANGE, path, 50); | ||
if (isThrottled) return this; | ||
} | ||
if (opts.alwaysStat && val1 === undefined && | ||
(event === EV_ADD || event === EV_ADD_DIR || event === EV_CHANGE) | ||
) { | ||
const fullPath = opts.cwd ? sysPath.join(opts.cwd, path) : path; | ||
let stats; | ||
try { | ||
stats = await stat(fullPath); | ||
} catch (err) {} | ||
// Suppress event when fs_stat fails, to avoid sending undefined 'stat' | ||
if (!stats || this.closed) return; | ||
args.push(stats); | ||
} | ||
this.emitWithAll(event, args); | ||
return this; | ||
} | ||
/** | ||
* Common handler for errors | ||
* @param {Error} error | ||
* @returns {Error|Boolean} The error if defined, otherwise the value of the FSWatcher instance's `closed` flag | ||
*/ | ||
_handleError(error) { | ||
const code = error && error.code; | ||
if (error && code !== 'ENOENT' && code !== 'ENOTDIR' && | ||
(!this.options.ignorePermissionErrors || (code !== 'EPERM' && code !== 'EACCES')) | ||
) { | ||
this.emit(EV_ERROR, error); | ||
} | ||
return error || this.closed; | ||
} | ||
/** | ||
* Helper utility for throttling | ||
* @param {ThrottleType} actionType type being throttled | ||
* @param {Path} path being acted upon | ||
* @param {Number} timeout duration of time to suppress duplicate actions | ||
* @returns {Object|false} tracking object or false if action should be suppressed | ||
*/ | ||
_throttle(actionType, path, timeout) { | ||
if (!this._throttled.has(actionType)) { | ||
this._throttled.set(actionType, new Map()); | ||
} | ||
/** @type {Map<Path, Object>} */ | ||
const action = this._throttled.get(actionType); | ||
/** @type {Object} */ | ||
const actionPath = action.get(path); | ||
if (actionPath) { | ||
actionPath.count++; | ||
return false; | ||
} | ||
let timeoutObject; | ||
const clear = () => { | ||
const item = action.get(path); | ||
const count = item ? item.count : 0; | ||
action.delete(path); | ||
clearTimeout(timeoutObject); | ||
if (item) clearTimeout(item.timeoutObject); | ||
return count; | ||
}; | ||
timeoutObject = setTimeout(clear, timeout); | ||
const thr = {timeoutObject, clear, count: 0}; | ||
action.set(path, thr); | ||
return thr; | ||
} | ||
_incrReadyCount() { | ||
return this._readyCount++; | ||
} | ||
/** | ||
* Awaits write operation to finish. | ||
* Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback. | ||
* @param {Path} path being acted upon | ||
* @param {Number} threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished | ||
* @param {EventName} event | ||
* @param {Function} awfEmit Callback to be called when ready for event to be emitted. | ||
*/ | ||
_awaitWriteFinish(path, threshold, event, awfEmit) { | ||
let timeoutHandler; | ||
let fullPath = path; | ||
if (this.options.cwd && !sysPath.isAbsolute(path)) { | ||
fullPath = sysPath.join(this.options.cwd, path); | ||
} | ||
const now = new Date(); | ||
const awaitWriteFinish = (prevStat) => { | ||
fs.stat(fullPath, (err, curStat) => { | ||
if (err || !this._pendingWrites.has(path)) { | ||
if (err && err.code !== 'ENOENT') awfEmit(err); | ||
return; | ||
} | ||
const now = Number(new Date()); | ||
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); | ||
awfEmit(undefined, curStat); | ||
} else { | ||
timeoutHandler = setTimeout( | ||
awaitWriteFinish, | ||
this.options.awaitWriteFinish.pollInterval, | ||
curStat | ||
); | ||
} | ||
}); | ||
}; | ||
if (!this._pendingWrites.has(path)) { | ||
this._pendingWrites.set(path, { | ||
lastChange: now, | ||
cancelWait: () => { | ||
this._pendingWrites.delete(path); | ||
clearTimeout(timeoutHandler); | ||
return event; | ||
} | ||
}); | ||
timeoutHandler = setTimeout( | ||
awaitWriteFinish, | ||
this.options.awaitWriteFinish.pollInterval | ||
); | ||
} | ||
} | ||
_getGlobIgnored() { | ||
return [...this._ignoredPaths.values()]; | ||
} | ||
/** | ||
* Determines whether user has asked to ignore this path. | ||
* @param {Path} path filepath or dir | ||
* @param {fs.Stats=} stats result of fs.stat | ||
* @returns {Boolean} | ||
*/ | ||
_isIgnored(path, stats) { | ||
if (this.options.atomic && DOT_RE.test(path)) return true; | ||
if (!this._userIgnored) { | ||
const {cwd} = this.options; | ||
const ign = this.options.ignored; | ||
const ignored = ign && ign.map(normalizeIgnored(cwd)); | ||
const paths = arrify(ignored) | ||
.filter((path) => typeof path === STRING_TYPE && !isGlob(path)) | ||
.map((path) => path + SLASH_GLOBSTAR); | ||
const list = this._getGlobIgnored().map(normalizeIgnored(cwd)).concat(ignored, paths); | ||
this._userIgnored = anymatch(list, undefined, ANYMATCH_OPTS); | ||
} | ||
return this._userIgnored([path, stats]); | ||
} | ||
_isntIgnored(path, stat) { | ||
return !this._isIgnored(path, stat); | ||
} | ||
/** | ||
* Provides a set of common helpers and properties relating to symlink and glob handling. | ||
* @param {Path} path file, directory, or glob pattern being watched | ||
* @param {Number=} depth at any depth > 0, this isn't a glob | ||
* @returns {WatchHelper} object containing helpers for this path | ||
*/ | ||
_getWatchHelpers(path, depth) { | ||
const watchPath = depth || this.options.disableGlobbing || !isGlob(path) ? path : globParent(path); | ||
const follow = this.options.followSymlinks; | ||
return new WatchHelper(path, watchPath, follow, this); | ||
} | ||
// Directory helpers | ||
// ----------------- | ||
/** | ||
* Provides directory tracking objects | ||
* @param {String} directory path of the directory | ||
* @returns {DirEntry} the directory's tracking object | ||
*/ | ||
_getWatchedDir(directory) { | ||
if (!this._boundRemove) this._boundRemove = this._remove.bind(this); | ||
const dir = sysPath.resolve(directory); | ||
if (!this._watched.has(dir)) this._watched.set(dir, new DirEntry(dir, this._boundRemove)); | ||
return this._watched.get(dir); | ||
} | ||
// File helpers | ||
// ------------ | ||
/** | ||
* Check for read permissions. | ||
* Based on this answer on SO: https://stackoverflow.com/a/11781404/1358405 | ||
* @param {fs.Stats} stats - object, result of fs_stat | ||
* @returns {Boolean} indicates whether the file can be read | ||
*/ | ||
_hasReadPermissions(stats) { | ||
if (this.options.ignorePermissionErrors) return true; | ||
// stats.mode may be bigint | ||
const md = stats && Number.parseInt(stats.mode, 10); | ||
const st = md & 0o777; | ||
const it = Number.parseInt(st.toString(8)[0], 10); | ||
return Boolean(4 & it); | ||
} | ||
/** | ||
* Handles emitting unlink events for | ||
* files and directories, and via recursion, for | ||
* files and directories within directories that are unlinked | ||
* @param {String} directory within which the following item is located | ||
* @param {String} item base path of item/directory | ||
* @returns {void} | ||
*/ | ||
_remove(directory, item, isDirectory) { | ||
// if what is being deleted is a directory, get that directory's paths | ||
// for recursive deleting and cleaning of watched object | ||
// if it is not a directory, nestedDirectoryChildren will be empty array | ||
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.options.useFsEvents && 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); | ||
const nestedDirectoryChildren = wp.getChildren(); | ||
// Recursively remove children directories / files. | ||
nestedDirectoryChildren.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.remove(item); | ||
// 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 === EV_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 = isDirectory ? EV_UNLINK_DIR : EV_UNLINK; | ||
if (wasTracked && !this._isIgnored(path)) this._emit(eventName, path); | ||
// Avoid conflicts if we later create another file with the same name | ||
if (!this.options.useFsEvents) { | ||
this._closePath(path); | ||
} | ||
} | ||
/** | ||
* Closes all watchers for a path | ||
* @param {Path} path | ||
*/ | ||
_closePath(path) { | ||
this._closeFile(path) | ||
const dir = sysPath.dirname(path); | ||
this._getWatchedDir(dir).remove(sysPath.basename(path)); | ||
} | ||
/** | ||
* Closes only file-specific watchers | ||
* @param {Path} path | ||
*/ | ||
_closeFile(path) { | ||
const closers = this._closers.get(path); | ||
if (!closers) return; | ||
closers.forEach(closer => closer()); | ||
this._closers.delete(path); | ||
} | ||
/** | ||
* | ||
* @param {Path} path | ||
* @param {Function} closer | ||
*/ | ||
_addPathCloser(path, closer) { | ||
if (!closer) return; | ||
let list = this._closers.get(path); | ||
if (!list) { | ||
list = []; | ||
this._closers.set(path, list); | ||
} | ||
list.push(closer); | ||
} | ||
_readdirp(root, opts) { | ||
if (this.closed) return; | ||
const options = {type: EV_ALL, alwaysStat: true, lstat: true, ...opts}; | ||
let stream = readdirp(root, options); | ||
this._streams.add(stream); | ||
stream.once(STR_CLOSE, () => { | ||
stream = undefined; | ||
}); | ||
stream.once(STR_END, () => { | ||
if (stream) { | ||
this._streams.delete(stream); | ||
stream = undefined; | ||
return this; | ||
} | ||
}); | ||
return stream; | ||
/** | ||
* Common handler for errors | ||
* @returns The error if defined, otherwise the value of the FSWatcher instance's `closed` flag | ||
*/ | ||
_handleError(error) { | ||
const code = error && error.code; | ||
if (error && | ||
code !== 'ENOENT' && | ||
code !== 'ENOTDIR' && | ||
(!this.options.ignorePermissionErrors || (code !== 'EPERM' && code !== 'EACCES'))) { | ||
this.emit(handler_js_1.EVENTS.ERROR, error); | ||
} | ||
return error || this.closed; | ||
} | ||
/** | ||
* Helper utility for throttling | ||
* @param actionType type being throttled | ||
* @param path being acted upon | ||
* @param timeout duration of time to suppress duplicate actions | ||
* @returns {Object|false} tracking object or false if action should be suppressed | ||
*/ | ||
_throttle(actionType, path, timeout) { | ||
if (!this._throttled.has(actionType)) { | ||
this._throttled.set(actionType, new Map()); | ||
} | ||
/** @type {Map<Path, Object>} */ | ||
const action = this._throttled.get(actionType); | ||
if (!action) | ||
throw new Error('invalid throttle'); | ||
/** @type {Object} */ | ||
const actionPath = action.get(path); | ||
if (actionPath) { | ||
actionPath.count++; | ||
return false; | ||
} | ||
// eslint-disable-next-line prefer-const | ||
let timeoutObject; | ||
const clear = () => { | ||
const item = action.get(path); | ||
const count = item ? item.count : 0; | ||
action.delete(path); | ||
clearTimeout(timeoutObject); | ||
if (item) | ||
clearTimeout(item.timeoutObject); | ||
return count; | ||
}; | ||
timeoutObject = setTimeout(clear, timeout); | ||
const thr = { timeoutObject, clear, count: 0 }; | ||
action.set(path, thr); | ||
return thr; | ||
} | ||
_incrReadyCount() { | ||
return this._readyCount++; | ||
} | ||
/** | ||
* Awaits write operation to finish. | ||
* Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback. | ||
* @param path being acted upon | ||
* @param threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished | ||
* @param event | ||
* @param awfEmit Callback to be called when ready for event to be emitted. | ||
*/ | ||
_awaitWriteFinish(path, threshold, event, awfEmit) { | ||
const awf = this.options.awaitWriteFinish; | ||
if (typeof awf !== 'object') | ||
return; | ||
const pollInterval = awf.pollInterval; | ||
let timeoutHandler; | ||
let fullPath = path; | ||
if (this.options.cwd && !sysPath.isAbsolute(path)) { | ||
fullPath = sysPath.join(this.options.cwd, path); | ||
} | ||
const now = new Date(); | ||
const writes = this._pendingWrites; | ||
function awaitWriteFinishFn(prevStat) { | ||
(0, fs_1.stat)(fullPath, (err, curStat) => { | ||
if (err || !writes.has(path)) { | ||
if (err && err.code !== 'ENOENT') | ||
awfEmit(err); | ||
return; | ||
} | ||
const now = Number(new Date()); | ||
if (prevStat && curStat.size !== prevStat.size) { | ||
writes.get(path).lastChange = now; | ||
} | ||
const pw = writes.get(path); | ||
const df = now - pw.lastChange; | ||
if (df >= threshold) { | ||
writes.delete(path); | ||
awfEmit(undefined, curStat); | ||
} | ||
else { | ||
timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat); | ||
} | ||
}); | ||
} | ||
if (!writes.has(path)) { | ||
writes.set(path, { | ||
lastChange: now, | ||
cancelWait: () => { | ||
writes.delete(path); | ||
clearTimeout(timeoutHandler); | ||
return event; | ||
}, | ||
}); | ||
timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval); | ||
} | ||
} | ||
/** | ||
* Determines whether user has asked to ignore this path. | ||
*/ | ||
_isIgnored(path, stats) { | ||
if (this.options.atomic && DOT_RE.test(path)) | ||
return true; | ||
if (!this._userIgnored) { | ||
const { cwd } = this.options; | ||
const ign = this.options.ignored; | ||
const ignored = (ign || []).map(normalizeIgnored(cwd)); | ||
const ignoredPaths = [...this._ignoredPaths]; | ||
const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored]; | ||
this._userIgnored = anymatch(list, undefined); | ||
} | ||
return this._userIgnored(path, stats); | ||
} | ||
_isntIgnored(path, stat) { | ||
return !this._isIgnored(path, stat); | ||
} | ||
/** | ||
* Provides a set of common helpers and properties relating to symlink handling. | ||
* @param path file or directory pattern being watched | ||
*/ | ||
_getWatchHelpers(path) { | ||
return new WatchHelper(path, this.options.followSymlinks, this); | ||
} | ||
// Directory helpers | ||
// ----------------- | ||
/** | ||
* Provides directory tracking objects | ||
* @param directory path of the directory | ||
*/ | ||
_getWatchedDir(directory) { | ||
const dir = sysPath.resolve(directory); | ||
if (!this._watched.has(dir)) | ||
this._watched.set(dir, new DirEntry(dir, this._boundRemove)); | ||
return this._watched.get(dir); | ||
} | ||
// File helpers | ||
// ------------ | ||
/** | ||
* Check for read permissions: https://stackoverflow.com/a/11781404/1358405 | ||
*/ | ||
_hasReadPermissions(stats) { | ||
if (this.options.ignorePermissionErrors) | ||
return true; | ||
return Boolean(Number(stats.mode) & 0o400); | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
_remove(directory, item, isDirectory) { | ||
// if what is being deleted is a directory, get that directory's paths | ||
// for recursive deleting and cleaning of watched object | ||
// if it is not a directory, nestedDirectoryChildren will be empty array | ||
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); | ||
const nestedDirectoryChildren = wp.getChildren(); | ||
// Recursively remove children directories / files. | ||
nestedDirectoryChildren.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.remove(item); | ||
// 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 === handler_js_1.EVENTS.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 = isDirectory ? handler_js_1.EVENTS.UNLINK_DIR : handler_js_1.EVENTS.UNLINK; | ||
if (wasTracked && !this._isIgnored(path)) | ||
this._emit(eventName, path); | ||
// Avoid conflicts if we later create another file with the same name | ||
this._closePath(path); | ||
} | ||
/** | ||
* Closes all watchers for a path | ||
*/ | ||
_closePath(path) { | ||
this._closeFile(path); | ||
const dir = sysPath.dirname(path); | ||
this._getWatchedDir(dir).remove(sysPath.basename(path)); | ||
} | ||
/** | ||
* Closes only file-specific watchers | ||
*/ | ||
_closeFile(path) { | ||
const closers = this._closers.get(path); | ||
if (!closers) | ||
return; | ||
closers.forEach((closer) => closer()); | ||
this._closers.delete(path); | ||
} | ||
_addPathCloser(path, closer) { | ||
if (!closer) | ||
return; | ||
let list = this._closers.get(path); | ||
if (!list) { | ||
list = []; | ||
this._closers.set(path, list); | ||
} | ||
list.push(closer); | ||
} | ||
_readdirp(root, opts) { | ||
if (this.closed) | ||
return; | ||
const options = { type: handler_js_1.EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 }; | ||
let stream = (0, readdirp_1.readdirp)(root, options); | ||
this._streams.add(stream); | ||
stream.once(handler_js_1.STR_CLOSE, () => { | ||
stream = undefined; | ||
}); | ||
stream.once(handler_js_1.STR_END, () => { | ||
if (stream) { | ||
this._streams.delete(stream); | ||
stream = undefined; | ||
} | ||
}); | ||
return stream; | ||
} | ||
} | ||
} | ||
// Export FSWatcher class | ||
exports.FSWatcher = FSWatcher; | ||
/** | ||
* Instantiates watcher with paths to be tracked. | ||
* @param {String|Array<String>} paths file/directory paths and/or globs | ||
* @param {Object=} options chokidar opts | ||
* @param paths file/directory paths | ||
* @param options chokidar opts | ||
* @returns an instance of FSWatcher for chaining. | ||
*/ | ||
const watch = (paths, options) => { | ||
const watcher = new FSWatcher(options); | ||
watcher.add(paths); | ||
return watcher; | ||
const watcher = new FSWatcher(options); | ||
watcher.add(paths); | ||
return watcher; | ||
}; | ||
exports.watch = watch; | ||
exports.default = { watch: exports.watch, FSWatcher }; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "chokidar", | ||
"description": "Minimal and efficient cross-platform file watching library", | ||
"version": "3.6.0", | ||
"version": "4.0.0", | ||
"homepage": "https://github.com/paulmillr/chokidar", | ||
"author": "Paul Miller (https://paulmillr.com)", | ||
"contributors": [ | ||
"Paul Miller (https://paulmillr.com)", | ||
"Elan Shanker" | ||
"files": [ | ||
"index.js", | ||
"index.d.ts", | ||
"index.d.ts.map", | ||
"index.js.map", | ||
"handler.js", | ||
"handler.d.ts", | ||
"handler.d.ts.map", | ||
"handler.js.map", | ||
"esm" | ||
], | ||
"engines": { | ||
"node": ">= 8.10.0" | ||
"main": "./index.js", | ||
"module": "./esm/index.js", | ||
"types": "./index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": "./esm/index.js", | ||
"require": "./index.js" | ||
}, | ||
"./handler.js": { | ||
"import": "./esm/handler.js", | ||
"require": "./handler.js" | ||
} | ||
}, | ||
"main": "index.js", | ||
"types": "./types/index.d.ts", | ||
"dependencies": { | ||
"anymatch": "~3.1.2", | ||
"braces": "~3.0.2", | ||
"glob-parent": "~5.1.2", | ||
"is-binary-path": "~2.1.0", | ||
"is-glob": "~4.0.1", | ||
"normalize-path": "~3.0.0", | ||
"readdirp": "~3.6.0" | ||
"readdirp": "^4.0.1" | ||
}, | ||
"optionalDependencies": { | ||
"fsevents": "~2.3.2" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^14", | ||
"chai": "^4.3", | ||
"dtslint": "^3.3.0", | ||
"eslint": "^7.0.0", | ||
"mocha": "^7.0.0", | ||
"rimraf": "^3.0.0", | ||
"sinon": "^9.0.1", | ||
"sinon-chai": "^3.3.0", | ||
"typescript": "^4.4.3", | ||
"upath": "^1.2.0" | ||
"@paulmillr/jsbt": "0.2.1", | ||
"@types/node": "20.14.8", | ||
"chai": "4.3.4", | ||
"prettier": "3.1.1", | ||
"rimraf": "5.0.5", | ||
"sinon": "12.0.1", | ||
"sinon-chai": "3.7.0", | ||
"typescript": "5.5.2", | ||
"upath": "2.0.1" | ||
}, | ||
"files": [ | ||
"index.js", | ||
"lib/*.js", | ||
"types/index.d.ts" | ||
], | ||
"sideEffects": false, | ||
"engines": { | ||
"node": ">= 14.16.0" | ||
}, | ||
"repository": { | ||
@@ -54,7 +58,6 @@ "type": "git", | ||
"scripts": { | ||
"dtslint": "dtslint types", | ||
"lint": "eslint --report-unused-disable-directives --ignore-path .gitignore .", | ||
"build": "npm ls", | ||
"mocha": "mocha --exit --timeout 90000", | ||
"test": "npm run lint && npm run mocha" | ||
"build": "tsc && tsc -p tsconfig.esm.json", | ||
"lint": "prettier --check src", | ||
"format": "prettier --write src", | ||
"test": "node --test" | ||
}, | ||
@@ -61,0 +64,0 @@ "keywords": [ |
138
README.md
@@ -1,56 +0,38 @@ | ||
# Chokidar [![Weekly downloads](https://img.shields.io/npm/dw/chokidar.svg)](https://github.com/paulmillr/chokidar) [![Yearly downloads](https://img.shields.io/npm/dy/chokidar.svg)](https://github.com/paulmillr/chokidar) | ||
# Chokidar [![Weekly downloads](https://img.shields.io/npm/dw/chokidar.svg)](https://github.com/paulmillr/chokidar) | ||
> Minimal and efficient cross-platform file watching library | ||
[![NPM](https://nodei.co/npm/chokidar.png)](https://www.npmjs.com/package/chokidar) | ||
## Why? | ||
Node.js `fs.watch`: | ||
There are many reasons to prefer Chokidar to raw fs.watch / fs.watchFile in 2024: | ||
* Doesn't report filenames on MacOS. | ||
* Doesn't report events at all when using editors like Sublime on MacOS. | ||
* Often reports events twice. | ||
* Emits most changes as `rename`. | ||
* Does not provide an easy way to recursively watch file trees. | ||
* Does not support recursive watching on Linux. | ||
- Events are properly reported | ||
- macOS events report filenames | ||
- events are not reported twice | ||
- changes are reported as add / change / unlink instead of useless `rename` | ||
- Atomic writes are supported, using `atomic` option | ||
- Some file editors use them | ||
- Chunked writes are supported, using `awaitWriteFinish` option | ||
- Large files are commonly written in chunks | ||
- File / dir filtering is supported | ||
- Symbolic links are supported | ||
- Recursive watching is always supported, instead of partial when using raw events | ||
- Includes a way to limit recursion depth | ||
Node.js `fs.watchFile`: | ||
* Almost as bad at event handling. | ||
* Also does not provide any recursive watching. | ||
* Results in high CPU utilization. | ||
Chokidar resolves these problems. | ||
Initially made for **[Brunch](https://brunch.io/)** (an ultra-swift web app build tool), it is now used in | ||
[Microsoft's Visual Studio Code](https://github.com/microsoft/vscode), | ||
[gulp](https://github.com/gulpjs/gulp/), | ||
[karma](https://karma-runner.github.io/), | ||
[PM2](https://github.com/Unitech/PM2), | ||
[browserify](http://browserify.org/), | ||
[webpack](https://webpack.github.io/), | ||
[BrowserSync](https://www.browsersync.io/), | ||
and [many others](https://www.npmjs.com/browse/depended/chokidar). | ||
It has proven itself in production environments. | ||
Version 3 is out! Check out our blog post about it: [Chokidar 3: How to save 32TB of traffic every week](https://paulmillr.com/posts/chokidar-3-save-32tb-of-traffic/) | ||
## How? | ||
Chokidar does still rely on the Node.js core `fs` module, but when using | ||
Chokidar relies on the Node.js core `fs` module, but when using | ||
`fs.watch` and `fs.watchFile` for watching, it normalizes the events it | ||
receives, often checking for truth by getting file stats and/or dir contents. | ||
On MacOS, chokidar by default uses a native extension exposing the Darwin | ||
`FSEvents` API. This provides very efficient recursive watching compared with | ||
implementations like `kqueue` available on most \*nix platforms. Chokidar still | ||
does have to do some work to normalize the events received that way as well. | ||
On most other platforms, the `fs.watch`-based implementation is the default, which | ||
The `fs.watch`-based implementation is the default, which | ||
avoids polling and keeps CPU usage down. Be advised that chokidar will initiate | ||
watchers recursively for everything within scope of the paths that have been | ||
specified, so be judicious about not wasting system resources by watching much | ||
more than needed. | ||
more than needed. For some cases, `fs.watchFile`, which utilizes polling and uses more resources, is used. | ||
Made for [Brunch](https://brunch.io/) in 2012, | ||
it is now used in [~30 million repositories](https://www.npmjs.com/browse/depended/chokidar) and | ||
has proven itself in production environments. | ||
**Sep 2024 update:** v4 is out! It decreases dependency count from 13 to 1, removes | ||
support for globs, adds support for ESM / Common.js modules, and bumps minimum node.js version from v8 to v14. | ||
## Getting started | ||
@@ -64,6 +46,6 @@ | ||
Then `require` and use it in your code: | ||
Use it in your code: | ||
```javascript | ||
const chokidar = require('chokidar'); | ||
import chokidar from 'chokidar'; | ||
@@ -74,11 +56,9 @@ // One-liner for current directory | ||
}); | ||
``` | ||
## API | ||
```javascript | ||
// Example of a more typical implementation structure | ||
// Extended options | ||
// ---------------- | ||
// Initialize watcher. | ||
const watcher = chokidar.watch('file, dir, glob, or array', { | ||
const watcher = chokidar.watch('file, dir, or array', { | ||
ignored: /(^|[\/\\])\../, // ignore dotfiles | ||
@@ -114,9 +94,9 @@ persistent: true | ||
watcher.add('new-file'); | ||
watcher.add(['new-file-2', 'new-file-3', '**/other-file*']); | ||
watcher.add(['new-file-2', 'new-file-3']); | ||
// Get list of actual paths being watched on the filesystem | ||
var watchedPaths = watcher.getWatched(); | ||
let watchedPaths = watcher.getWatched(); | ||
// Un-watch some files. | ||
await watcher.unwatch('new-file*'); | ||
await watcher.unwatch('new-file'); | ||
@@ -132,7 +112,6 @@ // Stop watching. | ||
ignored: '*.txt', | ||
ignored: (file) => file.endsWith('.txt'), | ||
ignoreInitial: false, | ||
followSymlinks: true, | ||
cwd: '.', | ||
disableGlobbing: false, | ||
@@ -158,8 +137,3 @@ usePolling: false, | ||
* `paths` (string or array of strings). Paths to files, dirs to be watched | ||
recursively, or glob patterns. | ||
- Note: globs must not contain windows separators (`\`), | ||
because that's how they work by the standard — | ||
you'll need to replace them with forward slashes (`/`). | ||
- Note 2: for additional glob documentation, check out low-level | ||
library: [picomatch](https://github.com/micromatch/picomatch). | ||
recursively. | ||
* `options` (object) Options object as defined below: | ||
@@ -190,4 +164,2 @@ | ||
derived. Paths emitted with events will be relative to this. | ||
* `disableGlobbing` (default: `false`). If set to `true` then the strings passed to `.watch()` and `.add()` are treated as | ||
literal path names, even if they look like globs. | ||
@@ -256,3 +228,3 @@ #### Performance | ||
* `.add(path / paths)`: Add files, directories, or glob patterns for tracking. | ||
* `.add(path / paths)`: Add files, directories for tracking. | ||
Takes an array of strings or just one string. | ||
@@ -264,3 +236,3 @@ * `.on(event, callback)`: Listen for an FS event. | ||
name and path for every event other than `ready`, `raw`, and `error`. `raw` is internal, use it carefully. | ||
* `.unwatch(path / paths)`: Stop watching files, directories, or glob patterns. | ||
* `.unwatch(path / paths)`: Stop watching files or directories. | ||
Takes an array of strings or just one string. | ||
@@ -276,34 +248,26 @@ * `.close()`: **async** Removes all listeners from watched files. Asynchronous, returns Promise. Use with `await` to ensure bugs don't happen. | ||
If you need a CLI interface for your file watching, check out | ||
[chokidar-cli](https://github.com/open-cli-tools/chokidar-cli), allowing you to | ||
third party [chokidar-cli](https://github.com/open-cli-tools/chokidar-cli), allowing you to | ||
execute a command on each change, or get a stdio stream of change events. | ||
## Install Troubleshooting | ||
## Troubleshooting | ||
* `npm WARN optional dep failed, continuing fsevents@n.n.n` | ||
* This message is normal part of how `npm` handles optional dependencies and is | ||
not indicative of a problem. Even if accompanied by other related error messages, | ||
Chokidar should function properly. | ||
* `TypeError: fsevents is not a constructor` | ||
* Update chokidar by doing `rm -rf node_modules package-lock.json yarn.lock && npm install`, or update your dependency that uses chokidar. | ||
* Chokidar is producing `ENOSP` error on Linux, like this: | ||
* `bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shell` | ||
* On Linux, sometimes there's `ENOSP` error: | ||
* `bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shell` | ||
`Error: watch /home/ ENOSPC` | ||
* This means Chokidar ran out of file handles and you'll need to increase their count by executing the following command in Terminal: | ||
* This means Chokidar ran out of file handles and you'll need to increase their count by executing the following command in Terminal: | ||
`echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p` | ||
* Upgrade to latest chokidar, to prevent fsevents-related issues: | ||
* `npm WARN optional dep failed, continuing fsevents@n.n.n` | ||
* `TypeError: fsevents is not a constructor` | ||
## Changelog | ||
For more detailed changelog, see [`full_changelog.md`](.github/full_changelog.md). | ||
- **v3.5 (Jan 6, 2021):** Support for ARM Macs with Apple Silicon. Fixes for deleted symlinks. | ||
- **v3.4 (Apr 26, 2020):** Support for directory-based symlinks. Fixes for macos file replacement. | ||
- **v3.3 (Nov 2, 2019):** `FSWatcher#close()` method became async. That fixes IO race conditions related to close method. | ||
- **v3.2 (Oct 1, 2019):** Improve Linux RAM usage by 50%. Race condition fixes. Windows glob fixes. Improve stability by using tight range of dependency versions. | ||
- **v3.1 (Sep 16, 2019):** dotfiles are no longer filtered out by default. Use `ignored` option if needed. Improve initial Linux scan time by 50%. | ||
- **v3 (Apr 30, 2019):** massive CPU & RAM consumption improvements; reduces deps / package size by a factor of 17x and bumps Node.js requirement to v8.16 and higher. | ||
- **v2 (Dec 29, 2017):** Globs are now posix-style-only; without windows support. Tons of bugfixes. | ||
- **v1 (Apr 7, 2015):** Glob support, symlink support, tons of bugfixes. Node 0.8+ is supported | ||
- **v0.1 (Apr 20, 2012):** Initial release, extracted from [Brunch](https://github.com/brunch/brunch/blob/9847a065aea300da99bd0753f90354cde9de1261/src/helpers.coffee#L66) | ||
- **v4 (Sep 2024):** remove glob support and bundled fsevents. Decrease dependency count from 13 to 1. Rewrite in typescript. Bumps minimum node.js requirement to v14+ | ||
- **v3 (Apr 2019):** massive CPU & RAM consumption improvements; reduces deps / package size by a factor of 17x and bumps Node.js requirement to v8.16+. | ||
- **v2 (Dec 2017):** globs are now posix-style-only. Tons of bugfixes. | ||
- **v1 (Apr 2015):** glob support, symlink support, tons of bugfixes. Node 0.8+ is supported | ||
- **v0.1 (Apr 2012):** Initial release, extracted from [Brunch](https://github.com/brunch/brunch/blob/9847a065aea300da99bd0753f90354cde9de1261/src/helpers.coffee#L66) | ||
Details in [`.github/full_changelog.md`](.github/full_changelog.md). | ||
## Also | ||
@@ -310,0 +274,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
256334
1
9
20
3468
2
273
12
1
+ Addedreaddirp@4.0.2(transitive)
- Removedanymatch@~3.1.2
- Removedbraces@~3.0.2
- Removedglob-parent@~5.1.2
- Removedis-binary-path@~2.1.0
- Removedis-glob@~4.0.1
- Removednormalize-path@~3.0.0
- Removedanymatch@3.1.3(transitive)
- Removedbinary-extensions@2.3.0(transitive)
- Removedbraces@3.0.3(transitive)
- Removedfill-range@7.1.1(transitive)
- Removedfsevents@2.3.3(transitive)
- Removedglob-parent@5.1.2(transitive)
- Removedis-binary-path@2.1.0(transitive)
- Removedis-extglob@2.1.1(transitive)
- Removedis-glob@4.0.3(transitive)
- Removedis-number@7.0.0(transitive)
- Removednormalize-path@3.0.0(transitive)
- Removedpicomatch@2.3.1(transitive)
- Removedreaddirp@3.6.0(transitive)
- Removedto-regex-range@5.0.1(transitive)
Updatedreaddirp@^4.0.1