Comparing version 2.1.5 to 3.0.0
1063
index.js
'use strict'; | ||
var EventEmitter = require('events').EventEmitter; | ||
var fs = require('fs'); | ||
var sysPath = require('path'); | ||
var asyncEach = require('async-each'); | ||
var anymatch = require('anymatch'); | ||
var globParent = require('glob-parent'); | ||
var isGlob = require('is-glob'); | ||
var isAbsolute = require('path-is-absolute'); | ||
var inherits = require('inherits'); | ||
var braces = require('braces'); | ||
var normalizePath = require('normalize-path'); | ||
var upath = require('upath'); | ||
const EventEmitter = require('events').EventEmitter; | ||
const fs = require('fs'); | ||
const sysPath = require('path'); | ||
const readdirp = require('readdirp'); | ||
const asyncEach = require('async-each'); | ||
const anymatch = require('anymatch'); | ||
const globParent = require('glob-parent'); | ||
const isGlob = require('is-glob'); | ||
const braces = require('braces'); | ||
const normalizePath = require('normalize-path'); | ||
var NodeFsHandler = require('./lib/nodefs-handler'); | ||
var FsEventsHandler = require('./lib/fsevents-handler'); | ||
const NodeFsHandler = require('./lib/nodefs-handler'); | ||
const FsEventsHandler = require('./lib/fsevents-handler'); | ||
var arrify = function(value) { | ||
if (value == null) return []; | ||
return Array.isArray(value) ? value : [value]; | ||
}; | ||
/** | ||
* @typedef {String} Path | ||
* @typedef {'all'|'add'|'addDir'|'change'|'unlink'|'unlinkDir'|'raw'|'error'|'ready'} EventName | ||
* @typedef {'readdir'|'watch'|'add'|'remove'|'change'} ThrottleType | ||
*/ | ||
var flatten = function(list, result) { | ||
if (result == null) result = []; | ||
list.forEach(function(item) { | ||
/** | ||
* | ||
* @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 | ||
*/ | ||
/** | ||
* @param {String|Array<String>} value | ||
*/ | ||
const arrify = (value = []) => Array.isArray(value) ? value : [value]; | ||
const flatten = (list, result = []) => { | ||
list.forEach(item => { | ||
if (Array.isArray(item)) { | ||
@@ -35,43 +52,106 @@ flatten(item, result); | ||
// Little isString util for use in Array#every. | ||
var isString = function(thing) { | ||
return typeof thing === 'string'; | ||
// Optimize RAM usage. | ||
const BACK_SLASH = /\\/g; | ||
const SLASH = '/'; | ||
const DOUBLE_SLASH = /\/\//; | ||
const BRACE_START = '{'; | ||
const BANG = '!'; | ||
const ONE_DOT = '.'; | ||
const TWO_DOTS = '..'; | ||
const DOT_RE = /\..*\.(sw[px])$|\~$|\.subl.*\.tmp/; | ||
const REPLACER_RE = /^\.[\/\\]/; | ||
const EMPTY_FN = () => {}; | ||
const toUnix = (string) => { | ||
let str = string.replace(BACK_SLASH, SLASH); | ||
while (str.match(DOUBLE_SLASH)) { | ||
str = str.replace(DOUBLE_SLASH, SLASH); | ||
} | ||
return str; | ||
}; | ||
// Public: Main class. | ||
// Watches files & directories for changes. | ||
// | ||
// * _opts - object, chokidar options hash | ||
// | ||
// Emitted events: | ||
// `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error` | ||
// | ||
// Examples | ||
// | ||
// var watcher = new FSWatcher() | ||
// .add(directories) | ||
// .on('add', path => console.log('File', path, 'was added')) | ||
// .on('change', path => console.log('File', path, 'was changed')) | ||
// .on('unlink', path => console.log('File', path, 'was removed')) | ||
// .on('all', (event, path) => console.log(path, ' emitted ', event)) | ||
// | ||
function FSWatcher(_opts) { | ||
EventEmitter.call(this); | ||
var opts = {}; | ||
// in case _opts that is passed in is a frozen object | ||
if (_opts) for (var opt in _opts) opts[opt] = _opts[opt]; | ||
this._watched = Object.create(null); | ||
this._closers = Object.create(null); | ||
this._ignoredPaths = Object.create(null); | ||
Object.defineProperty(this, '_globIgnored', { | ||
get: function() { return Object.keys(this._ignoredPaths); } | ||
}); | ||
this.closed = false; | ||
this._throttled = Object.create(null); | ||
this._symlinkPaths = Object.create(null); | ||
// Our version of upath.normalize | ||
// TODO: this is not equal to path-normalize module - investigate why | ||
const normalizePathToUnix = (path) => toUnix(sysPath.normalize(toUnix(path))); | ||
function undef(key) { | ||
return opts[key] === undefined; | ||
const normalizeIgnored = (cwd = '') => (path) => { | ||
if (typeof path !== 'string') return path; | ||
return normalizePathToUnix(sysPath.isAbsolute(path) ? path : sysPath.join(cwd, path)); | ||
}; | ||
/** | ||
* 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) { | ||
if (item !== ONE_DOT && item !== TWO_DOTS) this.items.add(item); | ||
} | ||
remove(item) { | ||
this.items.delete(item); | ||
if (!this.items.size) { | ||
const dir = this.path; | ||
fs.readdir(dir, err => { | ||
if (err) this._removeWatcher(sysPath.dirname(dir), sysPath.basename(dir)); | ||
}); | ||
} | ||
} | ||
has(item) { | ||
return this.items.has(item); | ||
} | ||
/** | ||
* @returns {Array<String>} | ||
*/ | ||
getChildren() { | ||
return Array.from(this.items.values()); | ||
} | ||
} | ||
/** | ||
* 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')) | ||
*/ | ||
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; | ||
const undef = (key) => opts[key] === undefined; | ||
// Set up default options. | ||
@@ -84,3 +164,3 @@ if (undef('persistent')) opts.persistent = true; | ||
if (undef('disableGlobbing')) opts.disableGlobbing = false; | ||
this.enableBinaryInterval = opts.binaryInterval !== opts.interval; | ||
opts.enableBinaryInterval = opts.binaryInterval !== opts.interval; | ||
@@ -91,6 +171,7 @@ // Enable fsevents on OS X when polling isn't explicitly enabled. | ||
// If we can't use fsevents, ensure the options reflect it's disabled. | ||
if (!FsEventsHandler.canUse()) opts.useFsEvents = false; | ||
const canUseFsEvents = FsEventsHandler.canUse(); | ||
if (!canUseFsEvents) opts.useFsEvents = false; | ||
// Use polling on Mac if not using fsevents. | ||
// Other platforms use non-polling fs.watch. | ||
// Other platforms use non-polling fs_watch. | ||
if (undef('usePolling') && !opts.useFsEvents) { | ||
@@ -102,5 +183,5 @@ opts.usePolling = process.platform === 'darwin'; | ||
// instances of chokidar, regardless of usage/dependency depth) | ||
var envPoll = process.env.CHOKIDAR_USEPOLLING; | ||
const envPoll = process.env.CHOKIDAR_USEPOLLING; | ||
if (envPoll !== undefined) { | ||
var envLower = envPoll.toLowerCase(); | ||
const envLower = envPoll.toLowerCase(); | ||
@@ -112,6 +193,6 @@ if (envLower === 'false' || envLower === '0') { | ||
} else { | ||
opts.usePolling = !!envLower | ||
opts.usePolling = !!envLower; | ||
} | ||
} | ||
var envInterval = process.env.CHOKIDAR_INTERVAL; | ||
const envInterval = process.env.CHOKIDAR_INTERVAL; | ||
if (envInterval) { | ||
@@ -123,3 +204,3 @@ opts.interval = parseInt(envInterval); | ||
if (undef('atomic')) opts.atomic = !opts.usePolling && !opts.useFsEvents; | ||
if (opts.atomic) this._pendingUnlinks = Object.create(null); | ||
if (opts.atomic) this._pendingUnlinks = new Map(); | ||
@@ -130,19 +211,15 @@ if (undef('followSymlinks')) opts.followSymlinks = true; | ||
if (opts.awaitWriteFinish === true) opts.awaitWriteFinish = {}; | ||
var awf = opts.awaitWriteFinish; | ||
const awf = opts.awaitWriteFinish; | ||
if (awf) { | ||
if (!awf.stabilityThreshold) awf.stabilityThreshold = 2000; | ||
if (!awf.pollInterval) awf.pollInterval = 100; | ||
this._pendingWrites = Object.create(null); | ||
this._pendingWrites = new Map(); | ||
} | ||
if (opts.ignored) opts.ignored = arrify(opts.ignored); | ||
this._isntIgnored = function(path, stat) { | ||
return !this._isIgnored(path, stat); | ||
}.bind(this); | ||
var readyCalls = 0; | ||
this._emitReady = function() { | ||
if (++readyCalls >= this._readyCount) { | ||
this._emitReady = Function.prototype; | ||
let readyCalls = 0; | ||
this._emitReady = () => { | ||
readyCalls++; | ||
if (readyCalls >= this._readyCount) { | ||
this._emitReady = EMPTY_FN; | ||
this._readyEmitted = true; | ||
@@ -152,6 +229,14 @@ // use process.nextTick to allow time for listener to be bound | ||
} | ||
}.bind(this); | ||
}; | ||
this._emitRaw = (...args) => this.emit('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. | ||
@@ -161,18 +246,169 @@ Object.freeze(opts); | ||
inherits(FSWatcher, EventEmitter); | ||
// 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 disableGlobbing = this.options.disableGlobbing; | ||
const cwd = this.options.cwd; | ||
this.closed = false; | ||
/** | ||
* @type {Array<String>} | ||
*/ | ||
let paths = flatten(arrify(paths_)); | ||
if (!paths.every(p => typeof p === 'string')) { | ||
throw new TypeError('Non-string provided as watch path: ' + paths); | ||
} | ||
if (cwd) paths = paths.map((path) => { | ||
let absPath; | ||
if (sysPath.isAbsolute(path)) { | ||
absPath = path; | ||
} else if (path[0] === BANG) { | ||
absPath = BANG + sysPath.join(cwd, path.substring(1)); | ||
} else { | ||
absPath = sysPath.join(cwd, path); | ||
} | ||
// Check `path` instead of `absPath` because the cwd portion can't be a glob | ||
if (disableGlobbing || !isGlob(path)) { | ||
return absPath; | ||
} else { | ||
return normalizePath(absPath); | ||
} | ||
}); | ||
// set aside negated glob strings | ||
paths = paths.filter((path) => { | ||
if (path[0] === BANG) { | ||
this._ignoredPaths.add(path.substring(1)); | ||
return false; | ||
} else { | ||
// if a path is being added that was previously ignored, stop ignoring it | ||
this._ignoredPaths.delete(path); | ||
this._ignoredPaths.delete(path + '/**'); | ||
// reset the cached userIgnored anymatch fn | ||
// to make ignoredPaths changes effective | ||
this._userIgnored = null; | ||
return true; | ||
} | ||
}); | ||
if (this.options.useFsEvents && this._fsEventsHandler) { | ||
if (!this._readyCount) this._readyCount = paths.length; | ||
if (this.options.persistent) this._readyCount *= 2; | ||
paths.forEach((path) => this._fsEventsHandler._addToFsEvents(path)); | ||
} else { | ||
if (!this._readyCount) this._readyCount = 0; | ||
this._readyCount += paths.length; | ||
asyncEach(paths, (path, next) => { | ||
this._nodeFsHandler._addToNodeFs(path, !_internal, 0, 0, _origAdd, (err, res) => { | ||
if (res) this._emitReady(); | ||
next(err, res); | ||
}); | ||
}, (error, results) => { | ||
results.forEach((item) => { | ||
if (!item || this.closed) return; | ||
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; | ||
paths = flatten(arrify(paths)); | ||
paths.forEach((path) => { | ||
// convert to absolute path unless relative path already matches | ||
if (!sysPath.isAbsolute(path) && !this._closers.has(path)) { | ||
const cwd = this.options.cwd; | ||
if (cwd) path = sysPath.join(cwd, path); | ||
path = sysPath.resolve(path); | ||
} | ||
this._closePath(path); | ||
this._ignoredPaths.add(path); | ||
if (this._watched.has(path)) { | ||
this._ignoredPaths.add(path + '/**'); | ||
} | ||
// reset the cached userIgnored anymatch fn | ||
// to make ignoredPaths changes effective | ||
this._userIgnored = null; | ||
}); | ||
return this; | ||
} | ||
/** | ||
* Close watchers and remove all listeners from watched paths. | ||
* @returns {FSWatcher} for chaining. | ||
*/ | ||
close() { | ||
if (this.closed) return this; | ||
this.closed = true; | ||
// Memory management. | ||
this._closers.forEach(closerList => closerList.forEach(closer => closer())); | ||
this._closers.clear(); | ||
this._watched.clear(); | ||
this._streams.forEach(stream => stream.destroy()); | ||
this._streams.clear(); | ||
this._symlinkPaths.clear(); | ||
this._throttled.clear(); | ||
this.removeAllListeners(); | ||
return this; | ||
} | ||
/** | ||
* 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; | ||
} | ||
// Common helpers | ||
// -------------- | ||
// Private method: Normalize and emit events | ||
// | ||
// * event - string, type of event | ||
// * path - string, file or directory path | ||
// * val[1..3] - arguments to be passed with event | ||
// | ||
// Returns the error if defined, otherwise the value of the | ||
// FSWatcher instance's `closed` flag | ||
FSWatcher.prototype._emit = function(event, path, val1, val2, val3) { | ||
if (this.options.cwd) path = sysPath.relative(this.options.cwd, path); | ||
var args = [event, path]; | ||
/** | ||
* 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 | ||
*/ | ||
_emit(event, path, val1, val2, val3) { | ||
const opts = this.options; | ||
if (opts.cwd) path = sysPath.relative(opts.cwd, path); | ||
/** @type Array<any> */ | ||
const args = [event, path]; | ||
if (val3 !== undefined) args.push(val1, val2, val3); | ||
@@ -182,34 +418,33 @@ else if (val2 !== undefined) args.push(val1, val2); | ||
var awf = this.options.awaitWriteFinish; | ||
if (awf && this._pendingWrites[path]) { | ||
this._pendingWrites[path].lastChange = new Date(); | ||
const awf = opts.awaitWriteFinish; | ||
let pw; | ||
if (awf && (pw = this._pendingWrites.get(path))) { | ||
pw.lastChange = new Date(); | ||
return this; | ||
} | ||
if (this.options.atomic) { | ||
if (opts.atomic) { | ||
if (event === 'unlink') { | ||
this._pendingUnlinks[path] = args; | ||
setTimeout(function() { | ||
Object.keys(this._pendingUnlinks).forEach(function(path) { | ||
this.emit.apply(this, this._pendingUnlinks[path]); | ||
this.emit.apply(this, ['all'].concat(this._pendingUnlinks[path])); | ||
delete this._pendingUnlinks[path]; | ||
}.bind(this)); | ||
}.bind(this), typeof this.options.atomic === "number" | ||
? this.options.atomic | ||
: 100); | ||
this._pendingUnlinks.set(path, args); | ||
setTimeout(() => { | ||
this._pendingUnlinks.forEach((entry, path) => { | ||
this.emit.apply(this, entry); | ||
this.emit.apply(this, ['all'].concat(entry)); | ||
this._pendingUnlinks.delete(path); | ||
}); | ||
}, typeof opts.atomic === "number" ? opts.atomic : 100); | ||
return this; | ||
} else if (event === 'add' && this._pendingUnlinks[path]) { | ||
} else if (event === 'add' && this._pendingUnlinks.has(path)) { | ||
event = args[0] = 'change'; | ||
delete this._pendingUnlinks[path]; | ||
this._pendingUnlinks.delete(path); | ||
} | ||
} | ||
var emitEvent = function() { | ||
const emitEvent = () => { | ||
this.emit.apply(this, args); | ||
if (event !== 'error') this.emit.apply(this, ['all'].concat(args)); | ||
}.bind(this); | ||
}; | ||
if (awf && (event === 'add' || event === 'change') && this._readyEmitted) { | ||
var awfEmit = function(err, stats) { | ||
const awfEmit = (err, stats) => { | ||
if (err) { | ||
@@ -235,12 +470,12 @@ event = args[0] = 'error'; | ||
if (event === 'change') { | ||
if (!this._throttle('change', path, 50)) return this; | ||
const isThrottled = !this._throttle('change', path, 50); | ||
if (isThrottled) return this; | ||
} | ||
if ( | ||
this.options.alwaysStat && val1 === undefined && | ||
if (opts.alwaysStat && val1 === undefined && | ||
(event === 'add' || event === 'addDir' || event === 'change') | ||
) { | ||
var fullPath = this.options.cwd ? sysPath.join(this.options.cwd, path) : path; | ||
fs.stat(fullPath, function(error, stats) { | ||
// Suppress event when fs.stat fails, to avoid sending undefined 'stat' | ||
const fullPath = opts.cwd ? sysPath.join(opts.cwd, path) : path; | ||
fs.stat(fullPath, (error, stats) => { | ||
// Suppress event when fs_stat fails, to avoid sending undefined 'stat' | ||
if (error || !stats) return; | ||
@@ -256,69 +491,81 @@ | ||
return this; | ||
}; | ||
} | ||
// Private method: Common handler for errors | ||
// | ||
// * error - object, Error instance | ||
// | ||
// Returns the error if defined, otherwise the value of the | ||
// FSWatcher instance's `closed` flag | ||
FSWatcher.prototype._handleError = function(error) { | ||
var code = error && error.code; | ||
var ipe = this.options.ignorePermissionErrors; | ||
if (error && | ||
code !== 'ENOENT' && | ||
code !== 'ENOTDIR' && | ||
(!ipe || (code !== 'EPERM' && code !== 'EACCES')) | ||
) this.emit('error', error); | ||
/** | ||
* 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('error', error); | ||
} | ||
return error || this.closed; | ||
}; | ||
} | ||
// Private method: Helper utility for throttling | ||
// | ||
// * action - string, type of action being throttled | ||
// * path - string, path being acted upon | ||
// * timeout - int, duration of time to suppress duplicate actions | ||
// | ||
// Returns throttle tracking object or false if action should be suppressed | ||
FSWatcher.prototype._throttle = function(action, path, timeout) { | ||
if (!(action in this._throttled)) { | ||
this._throttled[action] = Object.create(null); | ||
/** | ||
* 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()); | ||
} | ||
var throttled = this._throttled[action]; | ||
if (path in throttled) { | ||
throttled[path].count++; | ||
/** @type {Map<Path, Object>} */ | ||
const action = this._throttled.get(actionType); | ||
/** @type {Object} */ | ||
const actionPath = action.get(path); | ||
if (actionPath) { | ||
actionPath.count++; | ||
return false; | ||
} | ||
function clear() { | ||
var count = throttled[path] ? throttled[path].count : 0; | ||
delete throttled[path]; | ||
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; | ||
} | ||
var timeoutObject = setTimeout(clear, timeout); | ||
throttled[path] = {timeoutObject: timeoutObject, clear: clear, count: 0}; | ||
return throttled[path]; | ||
}; | ||
}; | ||
timeoutObject = setTimeout(clear, timeout); | ||
const thr = {timeoutObject: timeoutObject, clear: clear, count: 0}; | ||
action.set(path, thr); | ||
return thr; | ||
} | ||
// Private method: Awaits write operation to finish | ||
// | ||
// * path - string, path being acted upon | ||
// * threshold - int, time in milliseconds a file size must be fixed before | ||
// acknowledging write operation is finished | ||
// * awfEmit - function, to be called when ready for event to be emitted | ||
// Polls a newly created file for size variations. When files size does not | ||
// change for 'threshold' milliseconds calls callback. | ||
FSWatcher.prototype._awaitWriteFinish = function(path, threshold, event, awfEmit) { | ||
var timeoutHandler; | ||
_incrReadyCount() { | ||
return this._readyCount++; | ||
} | ||
var fullPath = path; | ||
if (this.options.cwd && !isAbsolute(path)) { | ||
/** | ||
* 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); | ||
} | ||
var now = new Date(); | ||
const now = new Date(); | ||
var awaitWriteFinish = (function (prevStat) { | ||
fs.stat(fullPath, function(err, curStat) { | ||
if (err || !(path in this._pendingWrites)) { | ||
const awaitWriteFinish = (prevStat) => { | ||
fs.stat(fullPath, (err, curStat) => { | ||
if (err || !this._pendingWrites.has(path)) { | ||
if (err && err.code !== 'ENOENT') awfEmit(err); | ||
@@ -328,10 +575,12 @@ return; | ||
var now = new Date(); | ||
const now = Number(new Date()); | ||
if (prevStat && curStat.size != prevStat.size) { | ||
this._pendingWrites[path].lastChange = now; | ||
this._pendingWrites.get(path).lastChange = now; | ||
} | ||
const pw = this._pendingWrites.get(path); | ||
const df = now - pw.lastChange; | ||
if (now - this._pendingWrites[path].lastChange >= threshold) { | ||
delete this._pendingWrites[path]; | ||
if (df >= threshold) { | ||
this._pendingWrites.delete(path); | ||
awfEmit(null, curStat); | ||
@@ -344,14 +593,14 @@ } else { | ||
} | ||
}.bind(this)); | ||
}.bind(this)); | ||
}); | ||
}; | ||
if (!(path in this._pendingWrites)) { | ||
this._pendingWrites[path] = { | ||
if (!this._pendingWrites.has(path)) { | ||
this._pendingWrites.set(path, { | ||
lastChange: now, | ||
cancelWait: function() { | ||
delete this._pendingWrites[path]; | ||
cancelWait: () => { | ||
this._pendingWrites.delete(path); | ||
clearTimeout(timeoutHandler); | ||
return event; | ||
}.bind(this) | ||
}; | ||
} | ||
}); | ||
timeoutHandler = setTimeout( | ||
@@ -362,31 +611,29 @@ awaitWriteFinish.bind(this), | ||
} | ||
}; | ||
} | ||
// Private method: Determines whether user has asked to ignore this path | ||
// | ||
// * path - string, path to file or directory | ||
// * stats - object, result of fs.stat | ||
// | ||
// Returns boolean | ||
var dotRe = /\..*\.(sw[px])$|\~$|\.subl.*\.tmp/; | ||
FSWatcher.prototype._isIgnored = function(path, stats) { | ||
if (this.options.atomic && dotRe.test(path)) return true; | ||
_getGlobIgnored() { | ||
return Array.from(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) { | ||
var cwd = this.options.cwd; | ||
var ignored = this.options.ignored; | ||
if (cwd && ignored) { | ||
ignored = ignored.map(function (path) { | ||
if (typeof path !== 'string') return path; | ||
return upath.normalize(isAbsolute(path) ? path : sysPath.join(cwd, path)); | ||
}); | ||
} | ||
var paths = arrify(ignored) | ||
.filter(function(path) { | ||
return typeof path === 'string' && !isGlob(path); | ||
}).map(function(path) { | ||
return path + '/**'; | ||
}); | ||
const cwd = this.options.cwd; | ||
const ign = this.options.ignored; | ||
const ignored = ign && ign.map(normalizeIgnored(cwd)); | ||
const paths = arrify(ignored) | ||
.filter((path) => typeof path === 'string' && !isGlob(path)) | ||
.map((path) => path + '/**'); | ||
this._userIgnored = anymatch( | ||
this._globIgnored.concat(ignored).concat(paths) | ||
this._getGlobIgnored() | ||
.map(normalizeIgnored(cwd)) | ||
.concat(ignored) | ||
.concat(paths) | ||
); | ||
@@ -396,22 +643,25 @@ } | ||
return this._userIgnored([path, stats]); | ||
}; | ||
} | ||
// Private method: Provides a set of common helpers and properties relating to | ||
// symlink and glob handling | ||
// | ||
// * path - string, file, directory, or glob pattern being watched | ||
// * depth - int, at any depth > 0, this isn't a glob | ||
// | ||
// Returns object containing helpers for this path | ||
var replacerRe = /^\.[\/\\]/; | ||
FSWatcher.prototype._getWatchHelpers = function(path, depth) { | ||
path = path.replace(replacerRe, ''); | ||
var watchPath = depth || this.options.disableGlobbing || !isGlob(path) ? path : globParent(path); | ||
var fullWatchPath = sysPath.resolve(watchPath); | ||
var hasGlob = watchPath !== path; | ||
var globFilter = hasGlob ? anymatch(path) : false; | ||
var follow = this.options.followSymlinks; | ||
var globSymlink = hasGlob && follow ? null : false; | ||
_isntIgnored(path, stat) { | ||
return !this._isIgnored(path, stat); | ||
} | ||
var checkGlobSymlink = function(entry) { | ||
/** | ||
* 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 {WatchHelpers} object containing helpers for this path | ||
*/ | ||
_getWatchHelpers(path, depth) { | ||
path = path.replace(REPLACER_RE, ''); | ||
const watchPath = depth || this.options.disableGlobbing || !isGlob(path) ? path : globParent(path); | ||
const fullWatchPath = sysPath.resolve(watchPath); | ||
const hasGlob = watchPath !== path; | ||
const globFilter = hasGlob ? anymatch(path) : false; | ||
const follow = this.options.followSymlinks; | ||
/** @type {any} */ | ||
let globSymlink = hasGlob && follow ? null : false; | ||
const checkGlobSymlink = (entry) => { | ||
// only need to resolve once | ||
@@ -433,3 +683,3 @@ // first entry should always have entry.parentDir === '' | ||
var entryPath = function(entry) { | ||
const entryPath = (entry) => { | ||
return sysPath.join(watchPath, | ||
@@ -440,16 +690,19 @@ sysPath.relative(watchPath, checkGlobSymlink(entry)) | ||
var filterPath = function(entry) { | ||
if (entry.stat && entry.stat.isSymbolicLink()) return filterDir(entry); | ||
var resolvedPath = entryPath(entry); | ||
return (!hasGlob || globFilter(resolvedPath)) && | ||
this._isntIgnored(resolvedPath, entry.stat) && | ||
(this.options.ignorePermissionErrors || | ||
this._hasReadPermissions(entry.stat)); | ||
}.bind(this); | ||
const filterPath = (entry) => { | ||
const {stats} = entry; | ||
if (stats && stats.isSymbolicLink()) return filterDir(entry); | ||
const resolvedPath = entryPath(entry); | ||
const matchesGlob = hasGlob ? globFilter(resolvedPath) : true; | ||
return matchesGlob && | ||
this._isntIgnored(resolvedPath, stats) && | ||
this._hasReadPermissions(stats); | ||
}; | ||
var getDirParts = function(path) { | ||
if (!hasGlob) return false; | ||
var parts = []; | ||
var expandedPath = braces.expand(path); | ||
expandedPath.forEach(function(path) { | ||
const getDirParts = (path) => { | ||
if (!hasGlob) return []; | ||
const parts = []; | ||
const expandedPath = path.includes(BRACE_START) | ||
? braces.expand(path) | ||
: [path]; | ||
expandedPath.forEach((path) => { | ||
parts.push(sysPath.relative(watchPath, path).split(/[\/\\]/)); | ||
@@ -460,16 +713,14 @@ }); | ||
var dirParts = getDirParts(path); | ||
if (dirParts) { | ||
dirParts.forEach(function(parts) { | ||
if (parts.length > 1) parts.pop(); | ||
}); | ||
} | ||
var unmatchedGlob; | ||
const dirParts = getDirParts(path); | ||
dirParts.forEach((parts) => { | ||
if (parts.length > 1) parts.pop(); | ||
}); | ||
let unmatchedGlob; | ||
var filterDir = function(entry) { | ||
const filterDir = (entry) => { | ||
if (hasGlob) { | ||
var entryParts = getDirParts(checkGlobSymlink(entry)); | ||
var globstar = false; | ||
unmatchedGlob = !dirParts.some(function(parts) { | ||
return parts.every(function(part, i) { | ||
const entryParts = getDirParts(checkGlobSymlink(entry)); | ||
let globstar = false; | ||
unmatchedGlob = !dirParts.some((parts) => { | ||
return parts.every((part, i) => { | ||
if (part === '**') globstar = true; | ||
@@ -480,4 +731,4 @@ return globstar || !entryParts[0][i] || anymatch(part, entryParts[0][i]); | ||
} | ||
return !unmatchedGlob && this._isntIgnored(entryPath(entry), entry.stat); | ||
}.bind(this); | ||
return !unmatchedGlob && this._isntIgnored(entryPath(entry), entry.stats); | ||
}; | ||
@@ -495,3 +746,3 @@ return { | ||
}; | ||
}; | ||
} | ||
@@ -501,28 +752,13 @@ // Directory helpers | ||
// Private method: Provides directory tracking objects | ||
// | ||
// * directory - string, path of the directory | ||
// | ||
// Returns the directory's tracking object | ||
FSWatcher.prototype._getWatchedDir = function(directory) { | ||
var dir = sysPath.resolve(directory); | ||
var watcherRemove = this._remove.bind(this); | ||
if (!(dir in this._watched)) this._watched[dir] = { | ||
_items: Object.create(null), | ||
add: function(item) { | ||
if (item !== '.' && item !== '..') this._items[item] = true; | ||
}, | ||
remove: function(item) { | ||
delete this._items[item]; | ||
if (!this.children().length) { | ||
fs.readdir(dir, function(err) { | ||
if (err) watcherRemove(sysPath.dirname(dir), sysPath.basename(dir)); | ||
}); | ||
} | ||
}, | ||
has: function(item) {return item in this._items;}, | ||
children: function() {return Object.keys(this._items);} | ||
}; | ||
return this._watched[dir]; | ||
}; | ||
/** | ||
* 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); | ||
} | ||
@@ -532,27 +768,31 @@ // File helpers | ||
// Private method: Check for read permissions | ||
// Based on this answer on SO: http://stackoverflow.com/a/11781404/1358405 | ||
// | ||
// * stats - object, result of fs.stat | ||
// | ||
// Returns boolean | ||
FSWatcher.prototype._hasReadPermissions = function(stats) { | ||
return Boolean(4 & parseInt(((stats && stats.mode) & 0x1ff).toString(8)[0], 10)); | ||
}; | ||
/** | ||
* Check for read permissions. | ||
* Based on this answer on SO: http://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; | ||
// Private method: Handles emitting unlink events for | ||
// files and directories, and via recursion, for | ||
// files and directories within directories that are unlinked | ||
// | ||
// * directory - string, directory within which the following item is located | ||
// * item - string, base path of item/directory | ||
// | ||
// Returns nothing | ||
FSWatcher.prototype._remove = function(directory, item) { | ||
const st = (stats && stats.mode) & 0o777; | ||
const it = 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) { | ||
// 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 | ||
var path = sysPath.join(directory, item); | ||
var fullPath = sysPath.resolve(path); | ||
var isDirectory = this._watched[path] || this._watched[fullPath]; | ||
const path = sysPath.join(directory, item); | ||
const fullPath = sysPath.resolve(path); | ||
const isDirectory = this._watched.has(path) || this._watched.has(fullPath); | ||
@@ -564,4 +804,3 @@ // prevent duplicate handling in case of arriving here nearly simultaneously | ||
// if the only watched file is removed, watch for its return | ||
var watchedDirs = Object.keys(this._watched); | ||
if (!isDirectory && !this.options.useFsEvents && watchedDirs.length === 1) { | ||
if (!isDirectory && !this.options.useFsEvents && this._watched.size === 1) { | ||
this.add(directory, item, true); | ||
@@ -572,19 +811,18 @@ } | ||
// so we got to do the directory check beforehand | ||
var nestedDirectoryChildren = this._getWatchedDir(path).children(); | ||
const wp = this._getWatchedDir(path); | ||
const nestedDirectoryChildren = wp.getChildren(); | ||
// Recursively remove children directories / files. | ||
nestedDirectoryChildren.forEach(function(nestedItem) { | ||
this._remove(path, nestedItem); | ||
}, this); | ||
nestedDirectoryChildren.forEach(nested => this._remove(path, nested)); | ||
// Check if item was on the watched list and remove it | ||
var parent = this._getWatchedDir(directory); | ||
var wasTracked = parent.has(item); | ||
const parent = this._getWatchedDir(directory); | ||
const wasTracked = parent.has(item); | ||
parent.remove(item); | ||
// If we wait for this file to be fully written, cancel the wait. | ||
var relPath = path; | ||
let relPath = path; | ||
if (this.options.cwd) relPath = sysPath.relative(this.options.cwd, path); | ||
if (this.options.awaitWriteFinish && this._pendingWrites[relPath]) { | ||
var event = this._pendingWrites[relPath].cancelWait(); | ||
if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) { | ||
const event = this._pendingWrites.get(relPath).cancelWait(); | ||
if (event === 'add') return; | ||
@@ -595,5 +833,5 @@ } | ||
// or a bogus entry to a file, in either case we have to remove it | ||
delete this._watched[path]; | ||
delete this._watched[fullPath]; | ||
var eventName = isDirectory ? 'unlinkDir' : 'unlink'; | ||
this._watched.delete(path); | ||
this._watched.delete(fullPath); | ||
const eventName = isDirectory ? 'unlinkDir' : 'unlink'; | ||
if (wasTracked && !this._isIgnored(path)) this._emit(eventName, path); | ||
@@ -605,166 +843,61 @@ | ||
} | ||
}; | ||
} | ||
FSWatcher.prototype._closePath = function(path) { | ||
if (!this._closers[path]) return; | ||
this._closers[path](); | ||
delete this._closers[path]; | ||
this._getWatchedDir(sysPath.dirname(path)).remove(sysPath.basename(path)); | ||
/** | ||
* | ||
* @param {Path} path | ||
*/ | ||
_closePath(path) { | ||
let closers = this._closers.get(path); | ||
if (!closers) return; | ||
closers.forEach(closer => closer()); | ||
this._closers.delete(path); | ||
closers = []; | ||
const dir = sysPath.dirname(path); | ||
this._getWatchedDir(dir).remove(sysPath.basename(path)); | ||
} | ||
// Public method: Adds paths to be watched on an existing FSWatcher instance | ||
// * paths - string or array of strings, file/directory paths and/or globs | ||
// * _origAdd - private boolean, for handling non-existent paths to be watched | ||
// * _internal - private boolean, indicates a non-user add | ||
// Returns an instance of FSWatcher for chaining. | ||
FSWatcher.prototype.add = function(paths, _origAdd, _internal) { | ||
var disableGlobbing = this.options.disableGlobbing; | ||
var cwd = this.options.cwd; | ||
this.closed = false; | ||
paths = flatten(arrify(paths)); | ||
if (!paths.every(isString)) { | ||
throw new TypeError('Non-string provided as watch path: ' + paths); | ||
/** | ||
* | ||
* @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); | ||
} | ||
if (cwd) paths = paths.map(function(path) { | ||
var absPath; | ||
if (isAbsolute(path)) { | ||
absPath = path; | ||
} else if (path[0] === '!') { | ||
absPath = '!' + sysPath.join(cwd, path.substring(1)); | ||
} else { | ||
absPath = sysPath.join(cwd, path); | ||
} | ||
// Check `path` instead of `absPath` because the cwd portion can't be a glob | ||
if (disableGlobbing || !isGlob(path)) { | ||
return absPath; | ||
} else { | ||
return normalizePath(absPath); | ||
} | ||
_readdirp(root, opts) { | ||
const options = Object.assign({type: 'all', alwaysStat: true, lstat: true}, opts); | ||
let stream = readdirp(root, options); | ||
this._streams.add(stream); | ||
stream.once('close', () => { | ||
stream = null; | ||
}); | ||
// set aside negated glob strings | ||
paths = paths.filter(function(path) { | ||
if (path[0] === '!') { | ||
this._ignoredPaths[path.substring(1)] = true; | ||
} else { | ||
// if a path is being added that was previously ignored, stop ignoring it | ||
delete this._ignoredPaths[path]; | ||
delete this._ignoredPaths[path + '/**']; | ||
// reset the cached userIgnored anymatch fn | ||
// to make ignoredPaths changes effective | ||
this._userIgnored = null; | ||
return true; | ||
stream.once('end', () => { | ||
if (stream) { | ||
this._streams.delete(stream); | ||
stream = null; | ||
} | ||
}, this); | ||
if (this.options.useFsEvents && FsEventsHandler.canUse()) { | ||
if (!this._readyCount) this._readyCount = paths.length; | ||
if (this.options.persistent) this._readyCount *= 2; | ||
paths.forEach(this._addToFsEvents, this); | ||
} else { | ||
if (!this._readyCount) this._readyCount = 0; | ||
this._readyCount += paths.length; | ||
asyncEach(paths, function(path, next) { | ||
this._addToNodeFs(path, !_internal, 0, 0, _origAdd, function(err, res) { | ||
if (res) this._emitReady(); | ||
next(err, res); | ||
}.bind(this)); | ||
}.bind(this), function(error, results) { | ||
results.forEach(function(item) { | ||
if (!item || this.closed) return; | ||
this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item)); | ||
}, this); | ||
}.bind(this)); | ||
} | ||
return this; | ||
}; | ||
// Public method: Close watchers or start ignoring events from specified paths. | ||
// * paths - string or array of strings, file/directory paths and/or globs | ||
// Returns instance of FSWatcher for chaining. | ||
FSWatcher.prototype.unwatch = function(paths) { | ||
if (this.closed) return this; | ||
paths = flatten(arrify(paths)); | ||
paths.forEach(function(path) { | ||
// convert to absolute path unless relative path already matches | ||
if (!isAbsolute(path) && !this._closers[path]) { | ||
if (this.options.cwd) path = sysPath.join(this.options.cwd, path); | ||
path = sysPath.resolve(path); | ||
} | ||
this._closePath(path); | ||
this._ignoredPaths[path] = true; | ||
if (path in this._watched) { | ||
this._ignoredPaths[path + '/**'] = true; | ||
} | ||
// reset the cached userIgnored anymatch fn | ||
// to make ignoredPaths changes effective | ||
this._userIgnored = null; | ||
}, this); | ||
return this; | ||
}; | ||
// Public method: Close watchers and remove all listeners from watched paths. | ||
// Returns instance of FSWatcher for chaining. | ||
FSWatcher.prototype.close = function() { | ||
if (this.closed) return this; | ||
this.closed = true; | ||
Object.keys(this._closers).forEach(function(watchPath) { | ||
this._closers[watchPath](); | ||
delete this._closers[watchPath]; | ||
}, this); | ||
this._watched = Object.create(null); | ||
this.removeAllListeners(); | ||
return this; | ||
}; | ||
// Public method: Expose list of watched paths | ||
// Returns object w/ dir paths as keys and arrays of contained paths as values. | ||
FSWatcher.prototype.getWatched = function() { | ||
var watchList = {}; | ||
Object.keys(this._watched).forEach(function(dir) { | ||
var key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir; | ||
watchList[key || '.'] = Object.keys(this._watched[dir]._items).sort(); | ||
}.bind(this)); | ||
return watchList; | ||
}; | ||
// Attach watch handler prototype methods | ||
function importHandler(handler) { | ||
Object.keys(handler.prototype).forEach(function(method) { | ||
FSWatcher.prototype[method] = handler.prototype[method]; | ||
}); | ||
return stream; | ||
} | ||
importHandler(NodeFsHandler); | ||
if (FsEventsHandler.canUse()) importHandler(FsEventsHandler); | ||
} | ||
// Export FSWatcher class | ||
exports.FSWatcher = FSWatcher; | ||
// Public function: Instantiates watcher with paths to be tracked. | ||
// * paths - string or array of strings, file/directory paths and/or globs | ||
// * options - object, chokidar options | ||
// Returns an instance of FSWatcher for chaining. | ||
exports.watch = function(paths, options) { | ||
return new FSWatcher(options).add(paths); | ||
}; | ||
/** | ||
* Instantiates watcher with paths to be tracked. | ||
* @param {String|Array<String>} paths file/directory paths and/or globs | ||
* @param {Object=} options chokidar opts | ||
* @returns an instance of FSWatcher for chaining. | ||
*/ | ||
const watch = (paths, options) => new FSWatcher(options).add(paths); | ||
exports.watch = watch; |
'use strict'; | ||
var fs = require('fs'); | ||
var sysPath = require('path'); | ||
var readdirp = require('readdirp'); | ||
var fsevents; | ||
const fs = require('fs'); | ||
const sysPath = require('path'); | ||
let fsevents; | ||
try { fsevents = require('fsevents'); } catch (error) { | ||
if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error) | ||
if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error); | ||
} | ||
// fsevents instance helper functions | ||
if (fsevents) { | ||
// TODO: real check | ||
let mtch = process.version.match(/v(\d+)\.(\d+)/); | ||
if (mtch && mtch[1] && mtch[2]) { | ||
let maj = parseInt(mtch[1]); | ||
let min = parseInt(mtch[2]); | ||
if (maj === 8 && min < 16) { | ||
fsevents = null; | ||
} | ||
} | ||
} | ||
// object to hold per-process fsevents instances | ||
// (may be shared across chokidar FSWatcher instances) | ||
var FSEventsWatchers = Object.create(null); | ||
const Option = (key, value) => isNaN(value) ? {} : {[key]: value}; | ||
// Threshold of duplicate path prefixes at which to start | ||
// consolidating going forward | ||
var consolidateThreshhold = 10; | ||
/** | ||
* @typedef {String} Path | ||
*/ | ||
// Private function: Instantiates the fsevents interface | ||
/** | ||
* @typedef {Object} FsEventsWatchContainer | ||
* @property {Set<Function>} listeners | ||
* @property {Function} rawEmitter | ||
* @property {{stop: Function}} watcher | ||
*/ | ||
// * path - string, path to be watched | ||
// * callback - function, called when fsevents is bound and ready | ||
// fsevents instance helper functions | ||
/** | ||
* Object to hold per-process fsevents instances (may be shared across chokidar FSWatcher instances) | ||
* @type {Map<Path,FsEventsWatchContainer>} | ||
*/ | ||
const FSEventsWatchers = new Map(); | ||
// Returns new fsevents instance | ||
function createFSEventsInstance(path, callback) { | ||
return (new fsevents(path)).on('fsevent', callback).start(); | ||
} | ||
// Threshold of duplicate path prefixes at which to start | ||
// consolidating going forward | ||
const consolidateThreshhold = 10; | ||
// Private function: Instantiates the fsevents interface or binds listeners | ||
// to an existing one covering the same file tree | ||
const wrongEventFlags = new Set([ | ||
69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912 | ||
]); | ||
// * path - string, path to be watched | ||
// * realPath - string, real path (in case of symlinks) | ||
// * listener - function, called when fsevents emits events | ||
// * rawEmitter - function, passes data to listeners of the 'raw' event | ||
/** | ||
* Instantiates the fsevents interface | ||
* @param {Path} path path to be watched | ||
* @param {Function} callback called when fsevents is bound and ready | ||
* @returns {{stop: Function}} new fsevents instance | ||
*/ | ||
const createFSEventsInstance = (path, callback) => { | ||
const stop = fsevents.watch(path, callback); | ||
return {stop}; | ||
}; | ||
// Returns close function | ||
/** | ||
* Instantiates the fsevents interface or binds listeners to an existing one covering | ||
* the same file tree. | ||
* @param {Path} path - to be watched | ||
* @param {Path} realPath - real path for symlinks | ||
* @param {Function} listener - called when fsevents emits events | ||
* @param {Function} rawEmitter - passes data to listeners of the 'raw' event | ||
* @returns {Function} closer | ||
*/ | ||
function setFSEventsListener(path, realPath, listener, rawEmitter) { | ||
var watchPath = sysPath.extname(path) ? sysPath.dirname(path) : path; | ||
var watchContainer; | ||
var parentPath = sysPath.dirname(watchPath); | ||
let watchPath = sysPath.extname(path) ? sysPath.dirname(path) : path; | ||
const parentPath = sysPath.dirname(watchPath); | ||
let cont = FSEventsWatchers.get(watchPath); | ||
@@ -53,4 +83,4 @@ // If we've accumulated a substantial number of paths that | ||
var resolvedPath = sysPath.resolve(path); | ||
var hasSymlink = resolvedPath !== realPath; | ||
const resolvedPath = sysPath.resolve(path); | ||
const hasSymlink = resolvedPath !== realPath; | ||
function filteredListener(fullPath, flags, info) { | ||
@@ -66,31 +96,29 @@ if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath); | ||
// modifies `watchPath` to the parent path when it finds a match | ||
function watchedParent() { | ||
return Object.keys(FSEventsWatchers).some(function(watchedPath) { | ||
// condition is met when indexOf returns 0 | ||
if (!realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep)) { | ||
const watchedParent = () => { | ||
for (const watchedPath of FSEventsWatchers.keys()) { | ||
if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) { | ||
watchPath = watchedPath; | ||
cont = FSEventsWatchers.get(watchPath); | ||
return true; | ||
} | ||
}); | ||
} | ||
} | ||
}; | ||
if (watchPath in FSEventsWatchers || watchedParent()) { | ||
watchContainer = FSEventsWatchers[watchPath]; | ||
watchContainer.listeners.push(filteredListener); | ||
if (cont || watchedParent()) { | ||
cont.listeners.add(filteredListener); | ||
} else { | ||
watchContainer = FSEventsWatchers[watchPath] = { | ||
listeners: [filteredListener], | ||
rawEmitters: [rawEmitter], | ||
watcher: createFSEventsInstance(watchPath, function(fullPath, flags) { | ||
var info = fsevents.getInfo(fullPath, flags); | ||
watchContainer.listeners.forEach(function(listener) { | ||
listener(fullPath, flags, info); | ||
cont = { | ||
listeners: new Set([filteredListener]), | ||
rawEmitter: rawEmitter, | ||
watcher: createFSEventsInstance(watchPath, (fullPath, flags) => { | ||
const info = fsevents.getInfo(fullPath, flags); | ||
cont.listeners.forEach(list => { | ||
list(fullPath, flags, info); | ||
}); | ||
watchContainer.rawEmitters.forEach(function(emitter) { | ||
emitter(info.event, fullPath, info); | ||
}); | ||
cont.rawEmitter(info.event, fullPath, info); | ||
}) | ||
}; | ||
FSEventsWatchers.set(watchPath, cont); | ||
} | ||
var listenerIndex = watchContainer.listeners.length - 1; | ||
@@ -100,7 +128,11 @@ // removes this instance's listeners and closes the underlying fsevents | ||
return function close() { | ||
delete watchContainer.listeners[listenerIndex]; | ||
delete watchContainer.rawEmitters[listenerIndex]; | ||
if (!Object.keys(watchContainer.listeners).length) { | ||
watchContainer.watcher.stop(); | ||
delete FSEventsWatchers[watchPath]; | ||
const wl = cont.listeners; | ||
wl.delete(filteredListener); | ||
if (!wl.size) { | ||
FSEventsWatchers.delete(watchPath); | ||
cont.watcher.stop(); | ||
cont.rawEmitter = cont.watcher = null; | ||
Object.freeze(cont); | ||
Object.freeze(cont.listeners); | ||
} | ||
@@ -112,8 +144,5 @@ }; | ||
// parent watcher | ||
function couldConsolidate(path) { | ||
var keys = Object.keys(FSEventsWatchers); | ||
var count = 0; | ||
for (var i = 0, len = keys.length; i < len; ++i) { | ||
var watchPath = keys[i]; | ||
const couldConsolidate = (path) => { | ||
let count = 0; | ||
for (const watchPath of FSEventsWatchers.keys()) { | ||
if (watchPath.indexOf(path) === 0) { | ||
@@ -128,37 +157,45 @@ count++; | ||
return false; | ||
} | ||
}; | ||
// returns boolean indicating whether fsevents can be used | ||
function canUse() { | ||
return fsevents && Object.keys(FSEventsWatchers).length < 128; | ||
} | ||
const canUse = () => fsevents && FSEventsWatchers.size < 128; | ||
// determines subdirectory traversal levels from root to path | ||
function depth(path, root) { | ||
var i = 0; | ||
const depth = (path, root) => { | ||
let i = 0; | ||
while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++; | ||
return i; | ||
} | ||
}; | ||
// fake constructor for attaching fsevents-specific prototype methods that | ||
// will be copied to FSWatcher's prototype | ||
function FsEventsHandler() {} | ||
/** | ||
* @mixin | ||
*/ | ||
class FsEventsHandler { | ||
// Private method: Handle symlinks encountered during directory scan | ||
/** | ||
* @param {FSWatcher} fsW | ||
*/ | ||
constructor(fsW) { | ||
const FSWatcher = require('../index').FSWatcher; | ||
this.fsw = fsW; | ||
} | ||
// * watchPath - string, file/dir path to be watched with fsevents | ||
// * realPath - string, real path (in case of symlinks) | ||
// * transform - function, path transformer | ||
// * globFilter - function, path filter in case a glob pattern was provided | ||
/** | ||
* Handle symlinks encountered during directory scan | ||
* @param {String} watchPath - file/dir path to be watched with fsevents | ||
* @param {String} realPath - real path (in case of symlinks) | ||
* @param {Function} transform - path transformer | ||
* @param {Function} globFilter - path filter in case a glob pattern was provided | ||
* @returns {Function} closer for the watcher instance | ||
*/ | ||
_watchWithFsEvents(watchPath, realPath, transform, globFilter) { | ||
// Returns close function for the watcher instance | ||
FsEventsHandler.prototype._watchWithFsEvents = | ||
function(watchPath, realPath, transform, globFilter) { | ||
if (this._isIgnored(watchPath)) return; | ||
var watchCallback = function(fullPath, flags, info) { | ||
if (this.fsw._isIgnored(watchPath)) return; | ||
const opts = this.fsw.options; | ||
const watchCallback = (fullPath, flags, info) => { | ||
if ( | ||
this.options.depth !== undefined && | ||
depth(fullPath, realPath) > this.options.depth | ||
opts.depth !== undefined && | ||
depth(fullPath, realPath) > opts.depth | ||
) return; | ||
var path = transform(sysPath.join( | ||
const path = transform(sysPath.join( | ||
watchPath, sysPath.relative(watchPath, fullPath) | ||
@@ -168,21 +205,22 @@ )); | ||
// ensure directories are tracked | ||
var parent = sysPath.dirname(path); | ||
var item = sysPath.basename(path); | ||
var watchedDir = this._getWatchedDir( | ||
const parent = sysPath.dirname(path); | ||
const item = sysPath.basename(path); | ||
const watchedDir = this.fsw._getWatchedDir( | ||
info.type === 'directory' ? path : parent | ||
); | ||
var checkIgnored = function(stats) { | ||
if (this._isIgnored(path, stats)) { | ||
this._ignoredPaths[path] = true; | ||
const checkIgnored = (stats) => { | ||
const ipaths = this.fsw._ignoredPaths; | ||
if (this.fsw._isIgnored(path, stats)) { | ||
ipaths.add(path); | ||
if (stats && stats.isDirectory()) { | ||
this._ignoredPaths[path + '/**/*'] = true; | ||
ipaths.add(path + '/**/*'); | ||
} | ||
return true; | ||
} else { | ||
delete this._ignoredPaths[path]; | ||
delete this._ignoredPaths[path + '/**/*']; | ||
ipaths.delete(path); | ||
ipaths.delete(path + '/**/*'); | ||
} | ||
}.bind(this); | ||
}; | ||
var handleEvent = function(event) { | ||
const handleEvent = (event) => { | ||
if (checkIgnored()) return; | ||
@@ -193,3 +231,3 @@ | ||
if (info.type === 'directory' || watchedDir.has(item)) { | ||
this._remove(parent, item); | ||
this.fsw._remove(parent, item); | ||
} | ||
@@ -199,7 +237,7 @@ } else { | ||
// track new directories | ||
if (info.type === 'directory') this._getWatchedDir(path); | ||
if (info.type === 'directory') this.fsw._getWatchedDir(path); | ||
if (info.type === 'symlink' && this.options.followSymlinks) { | ||
if (info.type === 'symlink' && opts.followSymlinks) { | ||
// push symlinks back to the top of the stack to get handled | ||
var curDepth = this.options.depth === undefined ? | ||
const curDepth = opts.depth === undefined ? | ||
undefined : depth(fullPath, realPath) + 1; | ||
@@ -210,10 +248,13 @@ return this._addToFsEvents(path, false, true, curDepth); | ||
// (other than symlinks being followed, which will be tracked soon) | ||
this._getWatchedDir(parent).add(item); | ||
this.fsw._getWatchedDir(parent).add(item); | ||
} | ||
} | ||
var eventName = info.type === 'directory' ? event + 'Dir' : event; | ||
this._emit(eventName, path); | ||
/** | ||
* @type {'add'|'addDir'|'unlink'|'unlinkDir'} | ||
*/ | ||
const eventName = info.type === 'directory' ? event + 'Dir' : event; | ||
this.fsw._emit(eventName, path); | ||
if (eventName === 'addDir') this._addToFsEvents(path, false, true); | ||
} | ||
}.bind(this); | ||
}; | ||
@@ -224,10 +265,16 @@ function addOrChange() { | ||
function checkFd() { | ||
fs.open(path, 'r', function(error, fd) { | ||
fs.open(path, 'r', function opened(error, fd) { | ||
if (error) { | ||
error.code !== 'EACCES' ? | ||
handleEvent('unlink') : addOrChange(); | ||
if (error.code !== 'EACCES') { | ||
handleEvent('unlink'); | ||
} else { | ||
addOrChange(); | ||
} | ||
} else { | ||
fs.close(fd, function(err) { | ||
err && err.code !== 'EACCES' ? | ||
handleEvent('unlink') : addOrChange(); | ||
fs.close(fd, function closed(err) { | ||
if (err && err.code !== 'EACCES') { | ||
handleEvent('unlink'); | ||
} else { | ||
addOrChange(); | ||
} | ||
}); | ||
@@ -238,8 +285,5 @@ } | ||
// correct for wrong events emitted | ||
var wrongEventFlags = [ | ||
69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912 | ||
]; | ||
if (wrongEventFlags.indexOf(flags) !== -1 || info.event === 'unknown') { | ||
if (typeof this.options.ignored === 'function') { | ||
fs.stat(path, function(error, stats) { | ||
if (wrongEventFlags.has(flags) || info.event === 'unknown') { | ||
if (typeof opts.ignored === 'function') { | ||
fs.stat(path, (error, stats) => { | ||
if (checkIgnored(stats)) return; | ||
@@ -261,43 +305,42 @@ stats ? addOrChange() : handleEvent('unlink'); | ||
} | ||
}.bind(this); | ||
}; | ||
var closer = setFSEventsListener( | ||
const closer = setFSEventsListener( | ||
watchPath, | ||
realPath, | ||
watchCallback, | ||
this.emit.bind(this, 'raw') | ||
this.fsw._emitRaw | ||
); | ||
this._emitReady(); | ||
this.fsw._emitReady(); | ||
return closer; | ||
}; | ||
} | ||
// Private method: Handle symlinks encountered during directory scan | ||
// * linkPath - string, path to symlink | ||
// * fullPath - string, absolute path to the symlink | ||
// * transform - function, pre-existing path transformer | ||
// * curDepth - int, level of subdirectories traversed to where symlink is | ||
// Returns nothing | ||
FsEventsHandler.prototype._handleFsEventsSymlink = | ||
function(linkPath, fullPath, transform, curDepth) { | ||
/** | ||
* Handle symlinks encountered during directory scan | ||
* @param {String} linkPath path to symlink | ||
* @param {String} fullPath absolute path to the symlink | ||
* @param {Function} transform pre-existing path transformer | ||
* @param {Number} curDepth level of subdirectories traversed to where symlink is | ||
* @returns {void} | ||
*/ | ||
_handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) { | ||
// don't follow the same symlink more than once | ||
if (this._symlinkPaths[fullPath]) return; | ||
else this._symlinkPaths[fullPath] = true; | ||
if (this.fsw._symlinkPaths.has(fullPath)) return; | ||
this._readyCount++; | ||
this.fsw._symlinkPaths.set(fullPath, true); | ||
this.fsw._incrReadyCount(); | ||
fs.realpath(linkPath, function(error, linkTarget) { | ||
if (this._handleError(error) || this._isIgnored(linkTarget)) { | ||
return this._emitReady(); | ||
fs.realpath(linkPath, (error, linkTarget) => { | ||
if (this.fsw._handleError(error) || this.fsw._isIgnored(linkTarget)) { | ||
return this.fsw._emitReady(); | ||
} | ||
this._readyCount++; | ||
this.fsw._incrReadyCount(); | ||
// add the linkTarget for watching with a wrapper for transform | ||
// that causes emitted paths to incorporate the link's path | ||
this._addToFsEvents(linkTarget || linkPath, function(path) { | ||
var dotSlash = '.' + sysPath.sep; | ||
var aliasedPath = linkPath; | ||
this._addToFsEvents(linkTarget || linkPath, (path) => { | ||
const dotSlash = '.' + sysPath.sep; | ||
let aliasedPath = linkPath; | ||
if (linkTarget && linkTarget !== dotSlash) { | ||
@@ -310,44 +353,51 @@ aliasedPath = path.replace(linkTarget, linkPath); | ||
}, false, curDepth); | ||
}.bind(this)); | ||
}; | ||
}); | ||
} | ||
// Private method: Handle added path with fsevents | ||
/** | ||
* Handle added path with fsevents | ||
* @param {String} path file/dir path or glob pattern | ||
* @param {Function|Boolean=} transform converts working path to what the user expects | ||
* @param {Boolean=} forceAdd ensure add is emitted | ||
* @param {Number=} priorDepth Level of subdirectories already traversed. | ||
* @returns {void} | ||
*/ | ||
_addToFsEvents(path, transform, forceAdd, priorDepth) { | ||
const opts = this.fsw.options; | ||
const processPath = typeof transform === 'function' ? transform : (val => val); | ||
// * path - string, file/directory path or glob pattern | ||
// * transform - function, converts working path to what the user expects | ||
// * forceAdd - boolean, ensure add is emitted | ||
// * priorDepth - int, level of subdirectories already traversed | ||
/** | ||
* | ||
* @param {Path} newPath | ||
* @param {fs.Stats} stats | ||
*/ | ||
const emitAdd = (newPath, stats) => { | ||
const pp = processPath(newPath); | ||
const isDir = stats.isDirectory(); | ||
const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp)); | ||
const base = sysPath.basename(pp); | ||
// Returns nothing | ||
FsEventsHandler.prototype._addToFsEvents = | ||
function(path, transform, forceAdd, priorDepth) { | ||
// applies transform if provided, otherwise returns same value | ||
var processPath = typeof transform === 'function' ? | ||
transform : function(val) { return val; }; | ||
var emitAdd = function(newPath, stats) { | ||
var pp = processPath(newPath); | ||
var isDir = stats.isDirectory(); | ||
var dirObj = this._getWatchedDir(sysPath.dirname(pp)); | ||
var base = sysPath.basename(pp); | ||
// ensure empty dirs get tracked | ||
if (isDir) this._getWatchedDir(pp); | ||
if (isDir) this.fsw._getWatchedDir(pp); | ||
if (dirObj.has(base)) return; | ||
dirObj.add(base); | ||
if (!this.options.ignoreInitial || forceAdd === true) { | ||
this._emit(isDir ? 'addDir' : 'add', pp, stats); | ||
if (!opts.ignoreInitial || forceAdd === true) { | ||
this.fsw._emit(isDir ? 'addDir' : 'add', pp, stats); | ||
} | ||
}.bind(this); | ||
}; | ||
var wh = this._getWatchHelpers(path); | ||
const wh = this.fsw._getWatchHelpers(path); | ||
// evaluate what is at the path we're being asked to watch | ||
fs[wh.statMethod](wh.watchPath, function(error, stats) { | ||
if (this._handleError(error) || this._isIgnored(wh.watchPath, stats)) { | ||
this._emitReady(); | ||
return this._emitReady(); | ||
fs[wh.statMethod](wh.watchPath, | ||
/** | ||
* @param {Error} error | ||
* @param {fs.Stats} stats | ||
*/ | ||
(error, stats) => { | ||
if (this.fsw._handleError(error) || this.fsw._isIgnored(wh.watchPath, stats)) { | ||
this.fsw._emitReady(); | ||
return this.fsw._emitReady(); | ||
} | ||
@@ -360,23 +410,20 @@ | ||
// don't recurse further if it would exceed depth setting | ||
if (priorDepth && priorDepth > this.options.depth) return; | ||
if (priorDepth && priorDepth > opts.depth) return; | ||
// scan the contents of the dir | ||
readdirp({ | ||
root: wh.watchPath, | ||
entryType: 'all', | ||
this.fsw._readdirp(wh.watchPath, { | ||
fileFilter: wh.filterPath, | ||
directoryFilter: wh.filterDir, | ||
lstat: true, | ||
depth: this.options.depth - (priorDepth || 0) | ||
}).on('data', function(entry) { | ||
...Option("depth", opts.depth - (priorDepth || 0)) | ||
}).on('data', (entry) => { | ||
// need to check filterPath on dirs b/c filterDir is less restrictive | ||
if (entry.stat.isDirectory() && !wh.filterPath(entry)) return; | ||
if (entry.stats.isDirectory() && !wh.filterPath(entry)) return; | ||
var joinedPath = sysPath.join(wh.watchPath, entry.path); | ||
var fullPath = entry.fullPath; | ||
const joinedPath = sysPath.join(wh.watchPath, entry.path); | ||
const fullPath = entry.fullPath; | ||
if (wh.followSymlinks && entry.stat.isSymbolicLink()) { | ||
if (wh.followSymlinks && entry.stats.isSymbolicLink()) { | ||
// preserve the current depth here since it can't be derived from | ||
// real paths past the symlink | ||
var curDepth = this.options.depth === undefined ? | ||
const curDepth = opts.depth === undefined ? | ||
undefined : depth(joinedPath, sysPath.resolve(wh.watchPath)) + 1; | ||
@@ -386,17 +433,17 @@ | ||
} else { | ||
emitAdd(joinedPath, entry.stat); | ||
emitAdd(joinedPath, entry.stats); | ||
} | ||
}.bind(this)).on('error', function() { | ||
// Ignore readdirp errors | ||
}).on('end', this._emitReady); | ||
}).on('error', () => {/* Ignore readdirp errors */}).on('end', () => { | ||
this.fsw._emitReady(); | ||
}); | ||
} else { | ||
emitAdd(wh.watchPath, stats); | ||
this._emitReady(); | ||
this.fsw._emitReady(); | ||
} | ||
}.bind(this)); | ||
}); | ||
if (this.options.persistent && forceAdd !== true) { | ||
var initWatch = function(error, realPath) { | ||
if (this.closed) return; | ||
var closer = this._watchWithFsEvents( | ||
if (opts.persistent && forceAdd !== true) { | ||
const initWatch = (error, realPath) => { | ||
if (this.fsw.closed) return; | ||
const closer = this._watchWithFsEvents( | ||
wh.watchPath, | ||
@@ -407,4 +454,4 @@ sysPath.resolve(realPath || wh.watchPath), | ||
); | ||
if (closer) this._closers[path] = closer; | ||
}.bind(this); | ||
this.fsw._addPathCloser(path, closer); | ||
}; | ||
@@ -418,5 +465,7 @@ if (typeof transform === 'function') { | ||
} | ||
}; | ||
} | ||
} | ||
module.exports = FsEventsHandler; | ||
module.exports.canUse = canUse; |
'use strict'; | ||
var fs = require('fs'); | ||
var sysPath = require('path'); | ||
var readdirp = require('readdirp'); | ||
var isBinaryPath = require('is-binary-path'); | ||
const fs = require('fs'); | ||
const sysPath = require('path'); | ||
const isBinaryPath = require('is-binary-path'); | ||
// fs.watch helpers | ||
/** | ||
* @typedef {String} Path | ||
*/ | ||
// object to hold per-process fs.watch instances | ||
// fs_watch helpers | ||
// object to hold per-process fs_watch instances | ||
// (may be shared across chokidar FSWatcher instances) | ||
var FsWatchInstances = Object.create(null); | ||
/** | ||
* @typedef {Object} FsWatchContainer | ||
* @property {Set} listeners | ||
* @property {Set} errHandlers | ||
* @property {Set} rawEmitters | ||
* @property {fs.FSWatcher=} watcher | ||
* @property {Boolean=} watcherUnusable | ||
*/ | ||
// Private function: Instantiates the fs.watch interface | ||
/** | ||
* @type {Map<String,FsWatchContainer>} | ||
*/ | ||
const FsWatchInstances = new Map(); | ||
const emptyFn = () => {}; | ||
// * path - string, path to be watched | ||
// * options - object, options to be passed to fs.watch | ||
// * listener - function, main event handler | ||
// * errHandler - function, handler which emits info about errors | ||
// * emitRaw - function, handler which emits raw event data | ||
// Returns new fsevents instance | ||
/** | ||
* Instantiates the fs_watch interface | ||
* @param {String} path to be watched | ||
* @param {Object} options to be passed to fs_watch | ||
* @param {Function} listener main event handler | ||
* @param {Function} errHandler emits info about errors | ||
* @param {Function} emitRaw emits raw event data | ||
* @returns {fs.FSWatcher} new fsevents instance | ||
*/ | ||
function createFsWatchInstance(path, options, listener, errHandler, emitRaw) { | ||
var handleEvent = function(rawEvent, evPath) { | ||
const handleEvent = (rawEvent, evPath) => { | ||
listener(path); | ||
@@ -44,32 +60,34 @@ emitRaw(rawEvent, evPath, {watchedPath: path}); | ||
// Private function: Helper for passing fs.watch event data to a | ||
// collection of listeners | ||
// * fullPath - string, absolute path bound to the fs.watch instance | ||
// * type - string, listener type | ||
// * val[1..3] - arguments to be passed to listeners | ||
// Returns nothing | ||
function fsWatchBroadcast(fullPath, type, val1, val2, val3) { | ||
if (!FsWatchInstances[fullPath]) return; | ||
FsWatchInstances[fullPath][type].forEach(function(listener) { | ||
/** | ||
* Helper for passing fs_watch event data to a collection of listeners | ||
* @param {Path} fullPath absolute path bound to fs_watch instance | ||
* @param {String} type listener type | ||
* @param {*=} val1 arguments to be passed to listeners | ||
* @param {*=} val2 | ||
* @param {*=} val3 | ||
*/ | ||
const fsWatchBroadcast = (fullPath, type, val1, val2, val3) => { | ||
const cont = FsWatchInstances.get(fullPath); | ||
if (!cont) return; | ||
cont[type].forEach((listener) => { | ||
listener(val1, val2, val3); | ||
}); | ||
} | ||
}; | ||
// Private function: Instantiates the fs.watch interface or binds listeners | ||
// to an existing one covering the same file system entry | ||
/** | ||
* Instantiates the fs_watch interface or binds listeners | ||
* to an existing one covering the same file system entry | ||
* @param {String} path | ||
* @param {String} fullPath absolute path | ||
* @param {Object} options to be passed to fs_watch | ||
* @param {Object} handlers container for event listener functions | ||
*/ | ||
const setFsWatchListener = (path, fullPath, options, handlers) => { | ||
const listener = handlers.listener; | ||
const errHandler = handlers.errHandler; | ||
const rawEmitter = handlers.rawEmitter; | ||
let cont = FsWatchInstances.get(fullPath); | ||
// * path - string, path to be watched | ||
// * fullPath - string, absolute path | ||
// * options - object, options to be passed to fs.watch | ||
// * handlers - object, container for event listener functions | ||
// Returns close function | ||
function setFsWatchListener(path, fullPath, options, handlers) { | ||
var listener = handlers.listener; | ||
var errHandler = handlers.errHandler; | ||
var rawEmitter = handlers.rawEmitter; | ||
var container = FsWatchInstances[fullPath]; | ||
var watcher; | ||
/** @type {fs.FSWatcher=} */ | ||
let watcher; | ||
if (!options.persistent) { | ||
@@ -81,3 +99,7 @@ watcher = createFsWatchInstance( | ||
} | ||
if (!container) { | ||
if (cont) { | ||
cont.listeners.add(listener); | ||
cont.errHandlers.add(errHandler); | ||
cont.rawEmitters.add(rawEmitter); | ||
} else { | ||
watcher = createFsWatchInstance( | ||
@@ -91,9 +113,9 @@ path, | ||
if (!watcher) return; | ||
var broadcastErr = fsWatchBroadcast.bind(null, fullPath, 'errHandlers'); | ||
watcher.on('error', function(error) { | ||
container.watcherUnusable = true; // documented since Node 10.4.1 | ||
const broadcastErr = fsWatchBroadcast.bind(null, fullPath, 'errHandlers'); | ||
watcher.on('error', (error) => { | ||
cont.watcherUnusable = true; // documented since Node 10.4.1 | ||
// Workaround for https://github.com/joyent/node/issues/4337 | ||
if (process.platform === 'win32' && error.code === 'EPERM') { | ||
fs.open(path, 'r', function(err, fd) { | ||
if (!err) fs.close(fd, function(err) { | ||
fs.open(path, 'r', (err, fd) => { | ||
if (!err) fs.close(fd, (err) => { | ||
if (!err) broadcastErr(error); | ||
@@ -106,57 +128,59 @@ }); | ||
}); | ||
container = FsWatchInstances[fullPath] = { | ||
listeners: [listener], | ||
errHandlers: [errHandler], | ||
rawEmitters: [rawEmitter], | ||
cont = { | ||
listeners: new Set([listener]), | ||
errHandlers: new Set([errHandler]), | ||
rawEmitters: new Set([rawEmitter]), | ||
watcher: watcher | ||
}; | ||
} else { | ||
container.listeners.push(listener); | ||
container.errHandlers.push(errHandler); | ||
container.rawEmitters.push(rawEmitter); | ||
FsWatchInstances.set(fullPath, cont); | ||
} | ||
var listenerIndex = container.listeners.length - 1; | ||
// const index = cont.listeners.indexOf(listener); | ||
// removes this instance's listeners and closes the underlying fs.watch | ||
// removes this instance's listeners and closes the underlying fs_watch | ||
// instance if there are no more listeners left | ||
return function close() { | ||
delete container.listeners[listenerIndex]; | ||
delete container.errHandlers[listenerIndex]; | ||
delete container.rawEmitters[listenerIndex]; | ||
if (!Object.keys(container.listeners).length) { | ||
if (!container.watcherUnusable) { // check to protect against issue #730 | ||
container.watcher.close(); | ||
} | ||
delete FsWatchInstances[fullPath]; | ||
cont.listeners.delete(listener); | ||
cont.errHandlers.delete(errHandler); | ||
cont.rawEmitters.delete(rawEmitter); | ||
if (!cont.listeners.size) { | ||
// Check to protect against issue gh-730. | ||
// if (cont.watcherUnusable) { | ||
cont.watcher.close(); | ||
// } | ||
FsWatchInstances.delete(fullPath); | ||
['listeners', 'errHandlers', 'rawEmitters'].forEach(key => { | ||
const set = cont[key]; | ||
set.clear(); | ||
Object.freeze(set); | ||
}); | ||
cont.watcher = null; | ||
Object.freeze(cont); | ||
} | ||
}; | ||
} | ||
}; | ||
// fs.watchFile helpers | ||
// fs_watchFile helpers | ||
// object to hold per-process fs.watchFile instances | ||
// object to hold per-process fs_watchFile instances | ||
// (may be shared across chokidar FSWatcher instances) | ||
var FsWatchFileInstances = Object.create(null); | ||
const FsWatchFileInstances = new Map(); | ||
// Private function: Instantiates the fs.watchFile interface or binds listeners | ||
// to an existing one covering the same file system entry | ||
/** | ||
* Instantiates the fs_watchFile interface or binds listeners | ||
* to an existing one covering the same file system entry | ||
* @param {String} path to be watched | ||
* @param {String} fullPath absolute path | ||
* @param {Object} options options to be passed to fs_watchFile | ||
* @param {Object} handlers container for event listener functions | ||
* @returns {Function} closer | ||
*/ | ||
const setFsWatchFileListener = (path, fullPath, options, handlers) => { | ||
const listener = handlers.listener; | ||
const rawEmitter = handlers.rawEmitter; | ||
let cont = FsWatchFileInstances.get(fullPath); | ||
let listeners = new Set(); | ||
let rawEmitters = new Set(); | ||
// * path - string, path to be watched | ||
// * fullPath - string, absolute path | ||
// * options - object, options to be passed to fs.watchFile | ||
// * handlers - object, container for event listener functions | ||
// Returns close function | ||
function setFsWatchFileListener(path, fullPath, options, handlers) { | ||
var listener = handlers.listener; | ||
var rawEmitter = handlers.rawEmitter; | ||
var container = FsWatchFileInstances[fullPath]; | ||
var listeners = []; | ||
var rawEmitters = []; | ||
if ( | ||
container && ( | ||
container.options.persistent < options.persistent || | ||
container.options.interval > options.interval | ||
) | ||
) { | ||
const copts = cont && cont.options; | ||
if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) { | ||
// "Upgrade" the watcher to persistence or a quicker interval. | ||
@@ -166,71 +190,84 @@ // This creates some unlikely edge case issues if the user mixes | ||
// doesn't seem worthwhile for the added complexity. | ||
listeners = container.listeners; | ||
rawEmitters = container.rawEmitters; | ||
listeners = cont.listeners; | ||
rawEmitters = cont.rawEmitters; | ||
fs.unwatchFile(fullPath); | ||
container = false; | ||
cont = null; | ||
} | ||
if (!container) { | ||
listeners.push(listener); | ||
rawEmitters.push(rawEmitter); | ||
container = FsWatchFileInstances[fullPath] = { | ||
if (cont) { | ||
cont.listeners.add(listener); | ||
cont.rawEmitters.add(rawEmitter); | ||
} else { | ||
listeners.add(listener); | ||
rawEmitters.add(rawEmitter); | ||
cont = { | ||
listeners: listeners, | ||
rawEmitters: rawEmitters, | ||
options: options, | ||
watcher: fs.watchFile(fullPath, options, function(curr, prev) { | ||
container.rawEmitters.forEach(function(rawEmitter) { | ||
watcher: fs.watchFile(fullPath, options, (curr, prev) => { | ||
cont.rawEmitters.forEach((rawEmitter) => { | ||
rawEmitter('change', fullPath, {curr: curr, prev: prev}); | ||
}); | ||
var currmtime = curr.mtime.getTime(); | ||
if (curr.size !== prev.size || currmtime > prev.mtime.getTime() || currmtime === 0) { | ||
container.listeners.forEach(function(listener) { | ||
listener(path, curr); | ||
}); | ||
const currmtime = curr.mtimeMs; | ||
if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) { | ||
cont.listeners.forEach((listener) => listener(path, curr)); | ||
} | ||
}) | ||
}; | ||
} else { | ||
container.listeners.push(listener); | ||
container.rawEmitters.push(rawEmitter); | ||
FsWatchFileInstances.set(fullPath, cont); | ||
} | ||
var listenerIndex = container.listeners.length - 1; | ||
// const index = cont.listeners.indexOf(listener); | ||
// removes this instance's listeners and closes the underlying fs.watchFile | ||
// instance if there are no more listeners left | ||
// Removes this instance's listeners and closes the underlying fs_watchFile | ||
// instance if there are no more listeners left. | ||
return function close() { | ||
delete container.listeners[listenerIndex]; | ||
delete container.rawEmitters[listenerIndex]; | ||
if (!Object.keys(container.listeners).length) { | ||
cont.listeners.delete(listener); | ||
cont.rawEmitters.delete(rawEmitter); | ||
if (!cont.listeners.size) { | ||
FsWatchFileInstances.delete(fullPath); | ||
fs.unwatchFile(fullPath); | ||
delete FsWatchFileInstances[fullPath]; | ||
cont.options = cont.watcher = null; | ||
Object.freeze(cont); | ||
Object.freeze(cont.listeners); | ||
} | ||
}; | ||
} | ||
}; | ||
// fake constructor for attaching nodefs-specific prototype methods that | ||
// will be copied to FSWatcher's prototype | ||
function NodeFsHandler() {} | ||
/** | ||
* @mixin | ||
*/ | ||
class NodeFsHandler { | ||
// Private method: Watch file for changes with fs.watchFile or fs.watch. | ||
/** | ||
* @param {FSWatcher} fsW | ||
*/ | ||
constructor(fsW) { | ||
const FSWatcher = require('../index').FSWatcher; | ||
this.fsw = fsW; | ||
this._boundHandleError = (error) => fsW._handleError(error); | ||
} | ||
// * path - string, path to file or directory. | ||
// * listener - function, to be executed on fs change. | ||
// Returns close function for the watcher instance | ||
NodeFsHandler.prototype._watchWithNodeFs = | ||
function(path, listener) { | ||
var directory = sysPath.dirname(path); | ||
var basename = sysPath.basename(path); | ||
var parent = this._getWatchedDir(directory); | ||
/** | ||
* Watch file for changes with fs_watchFile or fs_watch. | ||
* @param {String} path to file or dir | ||
* @param {Function} listener on fs change | ||
* @returns {Function} closer for the watcher instance | ||
*/ | ||
_watchWithNodeFs(path, listener) { | ||
const opts = this.fsw.options; | ||
const directory = sysPath.dirname(path); | ||
const basename = sysPath.basename(path); | ||
const parent = this.fsw._getWatchedDir(directory); | ||
parent.add(basename); | ||
var absolutePath = sysPath.resolve(path); | ||
var options = {persistent: this.options.persistent}; | ||
if (!listener) listener = Function.prototype; // empty function | ||
const absolutePath = sysPath.resolve(path); | ||
const options = {persistent: opts.persistent}; | ||
if (!listener) listener = emptyFn; | ||
var closer; | ||
if (this.options.usePolling) { | ||
options.interval = this.enableBinaryInterval && isBinaryPath(basename) ? | ||
this.options.binaryInterval : this.options.interval; | ||
let closer; | ||
if (opts.usePolling) { | ||
options.interval = opts.enableBinaryInterval && isBinaryPath(basename) ? | ||
opts.binaryInterval : opts.interval; | ||
closer = setFsWatchFileListener(path, absolutePath, options, { | ||
listener: listener, | ||
rawEmitter: this.emit.bind(this, 'raw') | ||
rawEmitter: this.fsw._emitRaw | ||
}); | ||
@@ -240,24 +277,23 @@ } else { | ||
listener: listener, | ||
errHandler: this._handleError.bind(this), | ||
rawEmitter: this.emit.bind(this, 'raw') | ||
errHandler: this._boundHandleError, | ||
rawEmitter: this.fsw._emitRaw | ||
}); | ||
} | ||
return closer; | ||
}; | ||
} | ||
// Private method: Watch a file and emit add event if warranted | ||
// * file - string, the file's path | ||
// * stats - object, result of fs.stat | ||
// * initialAdd - boolean, was the file added at watch instantiation? | ||
// * callback - function, called when done processing as a newly seen file | ||
// Returns close function for the watcher instance | ||
NodeFsHandler.prototype._handleFile = | ||
function(file, stats, initialAdd, callback) { | ||
var dirname = sysPath.dirname(file); | ||
var basename = sysPath.basename(file); | ||
var parent = this._getWatchedDir(dirname); | ||
/** | ||
* Watch a file and emit add event if warranted. | ||
* @param {Path} file Path | ||
* @param {fs.Stats} stats result of fs_stat | ||
* @param {Boolean} initialAdd was the file added at watch instantiation? | ||
* @param {Function} callback When done processing as a newly seen file | ||
* @returns {Function} closer for the watcher instance | ||
*/ | ||
_handleFile(file, stats, initialAdd, callback) { | ||
const dirname = sysPath.dirname(file); | ||
const basename = sysPath.basename(file); | ||
const parent = this.fsw._getWatchedDir(dirname); | ||
// stats is always present | ||
var prevStats = stats; | ||
let prevStats = stats; | ||
@@ -268,35 +304,35 @@ // if the file is already being watched, do nothing | ||
// kick off the watcher | ||
var closer = this._watchWithNodeFs(file, function(path, newStats) { | ||
if (!this._throttle('watch', file, 5)) return; | ||
if (!newStats || newStats && newStats.mtime.getTime() === 0) { | ||
fs.stat(file, function(error, newStats) { | ||
const closer = this._watchWithNodeFs(file, (path, newStats) => { | ||
if (!this.fsw._throttle('watch', file, 5)) return; | ||
if (!newStats || newStats && newStats.mtimeMs === 0) { | ||
fs.stat(file, (error, newStats) => { | ||
// Fix issues where mtime is null but file is still present | ||
if (error) { | ||
this._remove(dirname, basename); | ||
this.fsw._remove(dirname, basename); | ||
} else { | ||
// Check that change event was not fired because of changed only accessTime. | ||
var at = newStats.atime.getTime(); | ||
var mt = newStats.mtime.getTime(); | ||
if (!at || at <= mt || mt !== prevStats.mtime.getTime()) { | ||
this._emit('change', file, newStats); | ||
const at = newStats.atimeMs; | ||
const mt = newStats.mtimeMs; | ||
if (!at || at <= mt || mt !== prevStats.mtimeMs) { | ||
this.fsw._emit('change', file, newStats); | ||
} | ||
prevStats = newStats; | ||
} | ||
}.bind(this)); | ||
}); | ||
// 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. | ||
var at = newStats.atime.getTime(); | ||
var mt = newStats.mtime.getTime(); | ||
if (!at || at <= mt || mt !== prevStats.mtime.getTime()) { | ||
this._emit('change', file, newStats); | ||
const at = newStats.atimeMs; | ||
const mt = newStats.mtimeMs; | ||
if (!at || at <= mt || mt !== prevStats.mtimeMs) { | ||
this.fsw._emit('change', file, newStats); | ||
} | ||
prevStats = newStats; | ||
} | ||
}.bind(this)); | ||
}); | ||
// emit an add event if we're supposed to | ||
if (!(initialAdd && this.options.ignoreInitial)) { | ||
if (!this._throttle('add', file, 0)) return; | ||
this._emit('add', file, stats); | ||
if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) { | ||
if (!this.fsw._throttle('add', file, 0)) return; | ||
this.fsw._emit('add', file, stats); | ||
} | ||
@@ -306,33 +342,32 @@ | ||
return closer; | ||
}; | ||
} | ||
// Private method: Handle symlinks encountered while reading a dir | ||
/** | ||
* Handle symlinks encountered while reading a dir. | ||
* @param {Object} entry returned by readdirp | ||
* @param {String} directory path of dir being read | ||
* @param {String} path of this item | ||
* @param {String} item basename of this item | ||
* @returns {Boolean} true if no more processing is needed for this entry. | ||
*/ | ||
_handleSymlink(entry, directory, path, item) { | ||
const full = entry.fullPath; | ||
const dir = this.fsw._getWatchedDir(directory); | ||
// * entry - object, entry object returned by readdirp | ||
// * directory - string, path of the directory being read | ||
// * path - string, path of this item | ||
// * item - string, basename of this item | ||
// Returns true if no more processing is needed for this entry. | ||
NodeFsHandler.prototype._handleSymlink = | ||
function(entry, directory, path, item) { | ||
var full = entry.fullPath; | ||
var dir = this._getWatchedDir(directory); | ||
if (!this.options.followSymlinks) { | ||
if (!this.fsw.options.followSymlinks) { | ||
// watch symlink directly (don't follow) and detect changes | ||
this._readyCount++; | ||
fs.realpath(path, function(error, linkPath) { | ||
this.fsw._incrReadyCount(); | ||
fs.realpath(path, (error, linkPath) => { | ||
if (dir.has(item)) { | ||
if (this._symlinkPaths[full] !== linkPath) { | ||
this._symlinkPaths[full] = linkPath; | ||
this._emit('change', path, entry.stat); | ||
if (this.fsw._symlinkPaths.get(full) !== linkPath) { | ||
this.fsw._symlinkPaths.set(full, linkPath); | ||
this.fsw._emit('change', path, entry.stats); | ||
} | ||
} else { | ||
dir.add(item); | ||
this._symlinkPaths[full] = linkPath; | ||
this._emit('add', path, entry.stat); | ||
this.fsw._symlinkPaths.set(full, linkPath); | ||
this.fsw._emit('add', path, entry.stats); | ||
} | ||
this._emitReady(); | ||
}.bind(this)); | ||
this.fsw._emitReady(); | ||
}); | ||
return true; | ||
@@ -342,24 +377,25 @@ } | ||
// don't follow the same symlink more than once | ||
if (this._symlinkPaths[full]) return true; | ||
else this._symlinkPaths[full] = true; | ||
}; | ||
if (this.fsw._symlinkPaths.has(full)) { | ||
return true; | ||
} else { | ||
this.fsw._symlinkPaths.set(full, true); | ||
} | ||
} | ||
// Private method: Read directory to add / remove files from `@watched` list | ||
// and re-read it on change. | ||
// * dir - string, fs path. | ||
// * stats - object, result of fs.stat | ||
// * initialAdd - boolean, was the file added at watch instantiation? | ||
// * depth - int, depth relative to user-supplied path | ||
// * target - string, child path actually targeted for watch | ||
// * wh - object, common watch helpers for this path | ||
// * callback - function, called when dir scan is complete | ||
// Returns close function for the watcher instance | ||
NodeFsHandler.prototype._handleDir = | ||
function(dir, stats, initialAdd, depth, target, wh, callback) { | ||
var parentDir = this._getWatchedDir(sysPath.dirname(dir)); | ||
var tracked = parentDir.has(sysPath.basename(dir)); | ||
if (!(initialAdd && this.options.ignoreInitial) && !target && !tracked) { | ||
if (!wh.hasGlob || wh.globFilter(dir)) this._emit('addDir', dir, stats); | ||
/** | ||
* Read directory to add / remove files from `@watched` list and re-read it on change. | ||
* @param {String} dir fs path | ||
* @param {fs.Stats} stats | ||
* @param {Boolean} initialAdd | ||
* @param {Number} depth relative to user-supplied path | ||
* @param {String} target child path targetted for watch | ||
* @param {Object} wh Common watch helpers for this path | ||
* @param {Function} callback Called when dir scan is done | ||
* @returns {Function} closer for the watcher instance. | ||
*/ | ||
_handleDir(dir, stats, initialAdd, depth, target, wh, realpath, callback) { | ||
const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir)); | ||
const tracked = parentDir.has(sysPath.basename(dir)); | ||
if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) { | ||
if (!wh.hasGlob || wh.globFilter(dir)) this.fsw._emit('addDir', dir, stats); | ||
} | ||
@@ -369,5 +405,6 @@ | ||
parentDir.add(sysPath.basename(dir)); | ||
this._getWatchedDir(dir); | ||
this.fsw._getWatchedDir(dir); | ||
let throttler; | ||
var read = function(directory, initialAdd, done) { | ||
const read = (directory, initialAdd, done) => { | ||
// Normalize the directory name on Windows | ||
@@ -377,22 +414,19 @@ directory = sysPath.join(directory, ''); | ||
if (!wh.hasGlob) { | ||
var throttler = this._throttle('readdir', directory, 1000); | ||
if (!throttler) return; | ||
// throttler = this.fsw._throttle('readdir', directory, 1000); | ||
// if (!throttler) return; | ||
} | ||
var previous = this._getWatchedDir(wh.path); | ||
var current = []; | ||
const previous = this.fsw._getWatchedDir(wh.path); | ||
const current = new Set(); | ||
readdirp({ | ||
root: directory, | ||
entryType: 'all', | ||
this.fsw._readdirp(directory, { | ||
fileFilter: wh.filterPath, | ||
directoryFilter: wh.filterDir, | ||
depth: 0, | ||
lstat: true | ||
}).on('data', function(entry) { | ||
var item = entry.path; | ||
var path = sysPath.join(directory, item); | ||
current.push(item); | ||
}).on('data', (entry) => { | ||
const item = entry.path; | ||
let path = sysPath.join(directory, item); | ||
current.add(item); | ||
if (entry.stat.isSymbolicLink() && | ||
if (entry.stats.isSymbolicLink() && | ||
this._handleSymlink(entry, directory, path, item)) return; | ||
@@ -404,3 +438,3 @@ | ||
if (item === target || !target && !previous.has(item)) { | ||
this._readyCount++; | ||
this.fsw._incrReadyCount(); | ||
@@ -412,4 +446,5 @@ // ensure relativeness of path is preserved in case of watcher reuse | ||
} | ||
}.bind(this)).on('end', function() { | ||
var wasThrottled = throttler ? throttler.clear() : false; | ||
}).on('end', () => { | ||
const wasThrottled = throttler ? throttler.clear() : false; | ||
if (done) done(); | ||
@@ -420,5 +455,5 @@ | ||
// and are removed from @watched[directory]. | ||
previous.children().filter(function(item) { | ||
previous.getChildren().filter((item) => { | ||
return item !== directory && | ||
current.indexOf(item) === -1 && | ||
!current.has(item) && | ||
// in case of intersecting globs; | ||
@@ -430,18 +465,19 @@ // a path may have been filtered out of this readdir, but | ||
})); | ||
}).forEach(function(item) { | ||
this._remove(directory, item); | ||
}, this); | ||
}).forEach((item) => { | ||
this.fsw._remove(directory, item); | ||
}); | ||
// one more time for any missed in case changes came in extremely quickly | ||
if (wasThrottled) read(directory, false); | ||
}.bind(this)).on('error', this._handleError.bind(this)); | ||
}.bind(this); | ||
}).on('error', this._boundHandleError); | ||
}; | ||
var closer; | ||
let closer; | ||
if (this.options.depth == null || depth <= this.options.depth) { | ||
const oDepth = this.fsw.options.depth; | ||
if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath)) { | ||
if (!target) read(dir, initialAdd, callback); | ||
closer = this._watchWithNodeFs(dir, function(dirPath, stats) { | ||
closer = this._watchWithNodeFs(dir, (dirPath, stats) => { | ||
// if current directory is removed, do nothing | ||
if (stats && stats.mtime.getTime() === 0) return; | ||
if (stats && stats.mtimeMs === 0) return; | ||
@@ -454,19 +490,19 @@ read(dirPath, false); | ||
return closer; | ||
}; | ||
} | ||
// Private method: Handle added file, directory, or glob pattern. | ||
// Delegates call to _handleFile / _handleDir after checks. | ||
// * path - string, path to file or directory. | ||
// * initialAdd - boolean, was the file added at watch instantiation? | ||
// * depth - int, depth relative to user-supplied path | ||
// * target - string, child path actually targeted for watch | ||
// * callback - function, indicates whether the path was found or not | ||
// Returns nothing | ||
NodeFsHandler.prototype._addToNodeFs = | ||
function(path, initialAdd, priorWh, depth, target, callback) { | ||
/** | ||
* 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 targetted for watch | ||
* @param {String=} target Child path actually targeted for watch | ||
* @param {Function=} callback Indicates whetehr the path was found or not | ||
* @returns {void} | ||
*/ | ||
_addToNodeFs(path, initialAdd, priorWh, depth, target, callback) { | ||
if (!callback) callback = Function.prototype; | ||
var ready = this._emitReady; | ||
if (this._isIgnored(path) || this.closed) { | ||
const ready = this.fsw._emitReady; | ||
if (this.fsw._isIgnored(path) || this.fsw.closed) { | ||
ready(); | ||
@@ -476,3 +512,3 @@ return callback(null, false); | ||
var wh = this._getWatchHelpers(path, depth); | ||
const wh = this.fsw._getWatchHelpers(path, depth); | ||
if (!wh.hasGlob && priorWh) { | ||
@@ -486,5 +522,5 @@ wh.hasGlob = priorWh.hasGlob; | ||
// evaluate what is at the path we're being asked to watch | ||
fs[wh.statMethod](wh.watchPath, function(error, stats) { | ||
if (this._handleError(error)) return callback(null, path); | ||
if (this._isIgnored(wh.watchPath, stats)) { | ||
fs[wh.statMethod](wh.watchPath, (error, stats) => { | ||
if (this.fsw._handleError(error)) return callback(null, path); | ||
if (this.fsw._isIgnored(wh.watchPath, stats)) { | ||
ready(); | ||
@@ -494,29 +530,37 @@ return callback(null, false); | ||
var initDir = function(dir, target) { | ||
return this._handleDir(dir, stats, initialAdd, depth, target, wh, ready); | ||
}.bind(this); | ||
const initDir = (dir, target, realpath) => { | ||
return this._handleDir(dir, stats, initialAdd, depth, target, wh, realpath, ready); | ||
}; | ||
var closer; | ||
if (stats.isDirectory()) { | ||
closer = initDir(wh.watchPath, target); | ||
} else if (stats.isSymbolicLink()) { | ||
var parent = sysPath.dirname(wh.watchPath); | ||
this._getWatchedDir(parent).add(wh.watchPath); | ||
this._emit('add', wh.watchPath, stats); | ||
closer = initDir(parent, path); | ||
fs.realpath(path, (error, targetPath) => { | ||
let closer; | ||
if (stats.isDirectory()) { | ||
closer = initDir(wh.watchPath, target, targetPath); | ||
// preserve this symlink's target path | ||
if (path !== targetPath && targetPath !== undefined) { | ||
this.fsw._symlinkPaths.set(targetPath, true); | ||
} | ||
} else if (stats.isSymbolicLink()) { | ||
const parent = sysPath.dirname(wh.watchPath); | ||
this.fsw._getWatchedDir(parent).add(wh.watchPath); | ||
this.fsw._emit('add', wh.watchPath, stats); | ||
closer = initDir(parent, path, targetPath); | ||
// preserve this symlink's target path | ||
fs.realpath(path, function(error, targetPath) { | ||
this._symlinkPaths[sysPath.resolve(path)] = targetPath; | ||
// preserve this symlink's target path | ||
if (targetPath !== undefined) { | ||
this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath); | ||
} | ||
ready(); | ||
}.bind(this)); | ||
} else { | ||
closer = this._handleFile(wh.watchPath, stats, initialAdd, ready); | ||
} | ||
} else { | ||
closer = this._handleFile(wh.watchPath, stats, initialAdd, ready); | ||
} | ||
if (closer) this._closers[path] = closer; | ||
callback(null, false); | ||
}.bind(this)); | ||
}; | ||
this.fsw._addPathCloser(path, closer); | ||
callback(null, false); | ||
}); | ||
}); | ||
} | ||
} | ||
module.exports = NodeFsHandler; |
104
package.json
{ | ||
"name": "chokidar", | ||
"description": "A neat wrapper around node.js fs.watch / fs.watchFile / fsevents.", | ||
"version": "2.1.5", | ||
"keywords": [ | ||
"fs", | ||
"watch", | ||
"watchFile", | ||
"watcher", | ||
"watching", | ||
"file", | ||
"fsevents" | ||
"version": "3.0.0", | ||
"homepage": "https://github.com/paulmillr/chokidar", | ||
"author": "Paul Miller (https://paulmillr.com)", | ||
"contributors": [ | ||
"Paul Miller (https://paulmillr.com)", | ||
"Elan Shanker" | ||
], | ||
"types": "./types/index.d.ts", | ||
"homepage": "https://github.com/paulmillr/chokidar", | ||
"author": "Paul Miller (https://paulmillr.com), Elan Shanker", | ||
"engines": { | ||
"node": ">= 8" | ||
}, | ||
"dependencies": { | ||
"anymatch": "^3.0.1", | ||
"async-each": "^1.0.3", | ||
"braces": "^3.0.2", | ||
"glob-parent": "^5.0.0", | ||
"is-binary-path": "^2.1.0", | ||
"is-glob": "^4.0.1", | ||
"normalize-path": "^3.0.0", | ||
"readdirp": "^3.0.1" | ||
}, | ||
"optionalDependencies": { | ||
"fsevents": "^2.0.6" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^11.13.4", | ||
"chai": "^4.2.0", | ||
"coveralls": "^3.0.1", | ||
"dtslint": "0.4.1", | ||
"jshint": "^2.10.1", | ||
"mocha": "^6.1.3", | ||
"nyc": "^14.0.0", | ||
"rimraf": "^2.4.3", | ||
"sinon": "^7.3.1", | ||
"sinon-chai": "^3.3.0", | ||
"upath": "^1.1.2" | ||
}, | ||
"files": [ | ||
"index.js", | ||
"lib/", | ||
"types/index.d.ts" | ||
], | ||
"repository": { | ||
@@ -27,38 +55,28 @@ "type": "git", | ||
"test": "nyc mocha --exit", | ||
"mocha": "mocha", | ||
"lint": "jshint index.js lib", | ||
"coveralls": "nyc report --reporter=text-lcov | coveralls", | ||
"dtslint": "dtslint types" | ||
}, | ||
"files": [ | ||
"index.js", | ||
"lib/", | ||
"types/index.d.ts" | ||
"keywords": [ | ||
"fs", | ||
"watch", | ||
"watchFile", | ||
"watcher", | ||
"watching", | ||
"file", | ||
"fsevents" | ||
], | ||
"dependencies": { | ||
"anymatch": "^2.0.0", | ||
"async-each": "^1.0.1", | ||
"braces": "^2.3.2", | ||
"glob-parent": "^3.1.0", | ||
"inherits": "^2.0.3", | ||
"is-binary-path": "^1.0.0", | ||
"is-glob": "^4.0.0", | ||
"normalize-path": "^3.0.0", | ||
"path-is-absolute": "^1.0.0", | ||
"readdirp": "^2.2.1", | ||
"upath": "^1.1.1" | ||
}, | ||
"optionalDependencies": { | ||
"fsevents": "^1.2.7" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^11.9.4", | ||
"chai": "^3.2.0", | ||
"coveralls": "^3.0.1", | ||
"dtslint": "0.4.1", | ||
"graceful-fs": "4.1.4", | ||
"mocha": "^5.2.0", | ||
"nyc": "^11.8.0", | ||
"rimraf": "^2.4.3", | ||
"sinon": "^1.10.3", | ||
"sinon-chai": "^2.6.0" | ||
"types": "./types/index.d.ts", | ||
"jshintConfig": { | ||
"node": true, | ||
"curly": false, | ||
"bitwise": false, | ||
"mocha": true, | ||
"expr": true, | ||
"esversion": 8, | ||
"predef": [ | ||
"toString" | ||
] | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
# 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) [![Mac/Linux Build Status](https://img.shields.io/travis/paulmillr/chokidar/master.svg?label=Mac%20OSX%20%26%20Linux)](https://travis-ci.org/paulmillr/chokidar) [![Windows Build status](https://img.shields.io/appveyor/ci/paulmillr/chokidar/master.svg?label=Windows)](https://ci.appveyor.com/project/paulmillr/chokidar/branch/master) [![Coverage Status](https://coveralls.io/repos/paulmillr/chokidar/badge.svg)](https://coveralls.io/r/paulmillr/chokidar) | ||
# 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) | ||
@@ -14,3 +14,2 @@ > A neat wrapper around node.js fs.watch / fs.watchFile / FSEvents. | ||
* Emits most changes as `rename`. | ||
* Has [a lot of other issues](https://github.com/nodejs/node/search?q=fs.watch&type=Issues) | ||
* Does not provide an easy way to recursively watch file trees. | ||
@@ -27,2 +26,3 @@ | ||
Initially made for **[Brunch](http://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/), | ||
@@ -34,3 +34,2 @@ [karma](http://karma-runner.github.io), | ||
[BrowserSync](http://www.browsersync.io/), | ||
[Microsoft's Visual Studio Code](https://github.com/microsoft/vscode), | ||
and [many others](https://www.npmjs.org/browse/depended/chokidar/). | ||
@@ -65,3 +64,3 @@ It has proven itself in production environments. | ||
```javascript | ||
var chokidar = require('chokidar'); | ||
const chokidar = require('chokidar'); | ||
@@ -74,2 +73,4 @@ // One-liner for current directory, ignores .dotfiles | ||
## API | ||
```javascript | ||
@@ -79,3 +80,3 @@ // Example of a more typical implementation structure: | ||
// Initialize watcher. | ||
var watcher = chokidar.watch('file, dir, glob, or array', { | ||
const watcher = chokidar.watch('file, dir, glob, or array', { | ||
ignored: /(^|[\/\\])\../, | ||
@@ -86,3 +87,3 @@ persistent: true | ||
// Something to use when events are received. | ||
var log = console.log.bind(console); | ||
const log = console.log.bind(console); | ||
// Add event listeners. | ||
@@ -149,4 +150,2 @@ watcher | ||
## API | ||
`chokidar.watch(paths, [options])` | ||
@@ -280,24 +279,14 @@ | ||
## License | ||
The MIT License (MIT) | ||
## Changelog | ||
Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com) & Elan Shanker | ||
For more detailed changelog, see `.github/full_changelog.md` | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the “Software”), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
- v3 (Apr 30, 2019): Massive CPU & RAM consumption improvements. 17x package & deps size reduction. Node 8+-only | ||
- 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) | ||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
## License | ||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. | ||
MIT (c) Paul Miller (https://paulmillr.com), see LICENSE file. |
@@ -8,11 +8,5 @@ // TypeScript Version: 3.0 | ||
/** | ||
* The object's keys are all the directories (using absolute paths unless the `cwd` option was | ||
* used), and the values are arrays of the names of the items contained in each directory. | ||
*/ | ||
export interface WatchedPaths { | ||
[directory: string]: string[]; | ||
} | ||
export class FSWatcher extends EventEmitter implements fs.FSWatcher { | ||
options: WatchOptions; | ||
export class FSWatcher extends EventEmitter implements fs.FSWatcher { | ||
/** | ||
@@ -41,3 +35,5 @@ * Constructs a new FSWatcher instance with optional WatchOptions parameter. | ||
*/ | ||
getWatched(): WatchedPaths; | ||
getWatched(): { | ||
[directory: string]: string[]; | ||
}; | ||
@@ -44,0 +40,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
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
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
9
1811
0
78624
11
284
1
+ Addedanymatch@3.1.3(transitive)
+ Addedbinary-extensions@2.3.0(transitive)
+ Addedbraces@3.0.3(transitive)
+ Addedfill-range@7.1.1(transitive)
+ Addedfsevents@2.3.3(transitive)
+ Addedglob-parent@5.1.2(transitive)
+ Addedis-binary-path@2.1.0(transitive)
+ Addedis-number@7.0.0(transitive)
+ Addedpicomatch@2.3.1(transitive)
+ Addedreaddirp@3.6.0(transitive)
+ Addedto-regex-range@5.0.1(transitive)
- Removedinherits@^2.0.3
- Removedpath-is-absolute@^1.0.0
- Removedupath@^1.1.1
- Removedanymatch@2.0.0(transitive)
- Removedarr-diff@4.0.0(transitive)
- Removedarr-flatten@1.1.0(transitive)
- Removedarr-union@3.1.0(transitive)
- Removedarray-unique@0.3.2(transitive)
- Removedassign-symbols@1.0.0(transitive)
- Removedatob@2.1.2(transitive)
- Removedbase@0.11.2(transitive)
- Removedbinary-extensions@1.13.1(transitive)
- Removedbindings@1.5.0(transitive)
- Removedbraces@2.3.2(transitive)
- Removedcache-base@1.0.1(transitive)
- Removedclass-utils@0.3.6(transitive)
- Removedcollection-visit@1.0.0(transitive)
- Removedcomponent-emitter@1.3.1(transitive)
- Removedcopy-descriptor@0.1.1(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removeddebug@2.6.9(transitive)
- Removeddecode-uri-component@0.2.2(transitive)
- Removeddefine-property@0.2.51.0.02.0.2(transitive)
- Removedexpand-brackets@2.1.4(transitive)
- Removedextend-shallow@2.0.13.0.2(transitive)
- Removedextglob@2.0.4(transitive)
- Removedfile-uri-to-path@1.0.0(transitive)
- Removedfill-range@4.0.0(transitive)
- Removedfor-in@1.0.2(transitive)
- Removedfragment-cache@0.2.1(transitive)
- Removedfsevents@1.2.13(transitive)
- Removedfunction-bind@1.1.2(transitive)
- Removedget-value@2.0.6(transitive)
- Removedglob-parent@3.1.0(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedhas-value@0.3.11.0.0(transitive)
- Removedhas-values@0.1.41.0.0(transitive)
- Removedhasown@2.0.2(transitive)
- Removedinherits@2.0.4(transitive)
- Removedis-accessor-descriptor@1.0.1(transitive)
- Removedis-binary-path@1.0.1(transitive)
- Removedis-buffer@1.1.6(transitive)
- Removedis-data-descriptor@1.0.1(transitive)
- Removedis-descriptor@0.1.71.0.3(transitive)
- Removedis-extendable@0.1.11.0.1(transitive)
- Removedis-glob@3.1.0(transitive)
- Removedis-number@3.0.0(transitive)
- Removedis-plain-object@2.0.4(transitive)
- Removedis-windows@1.0.2(transitive)
- Removedisarray@1.0.0(transitive)
- Removedisobject@2.1.03.0.1(transitive)
- Removedkind-of@3.2.24.0.06.0.3(transitive)
- Removedmap-cache@0.2.2(transitive)
- Removedmap-visit@1.0.0(transitive)
- Removedmicromatch@3.1.10(transitive)
- Removedmixin-deep@1.3.2(transitive)
- Removedms@2.0.0(transitive)
- Removednan@2.20.0(transitive)
- Removednanomatch@1.2.13(transitive)
- Removednormalize-path@2.1.1(transitive)
- Removedobject-copy@0.1.0(transitive)
- Removedobject-visit@1.0.1(transitive)
- Removedobject.pick@1.3.0(transitive)
- Removedpascalcase@0.1.1(transitive)
- Removedpath-dirname@1.0.2(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedposix-character-classes@0.1.1(transitive)
- Removedprocess-nextick-args@2.0.1(transitive)
- Removedreadable-stream@2.3.8(transitive)
- Removedreaddirp@2.2.1(transitive)
- Removedregex-not@1.0.2(transitive)
- Removedremove-trailing-separator@1.1.0(transitive)
- Removedrepeat-element@1.1.4(transitive)
- Removedrepeat-string@1.6.1(transitive)
- Removedresolve-url@0.2.1(transitive)
- Removedret@0.1.15(transitive)
- Removedsafe-buffer@5.1.2(transitive)
- Removedsafe-regex@1.1.0(transitive)
- Removedset-value@2.0.1(transitive)
- Removedsnapdragon@0.8.2(transitive)
- Removedsnapdragon-node@2.1.1(transitive)
- Removedsnapdragon-util@3.0.1(transitive)
- Removedsource-map@0.5.7(transitive)
- Removedsource-map-resolve@0.5.3(transitive)
- Removedsource-map-url@0.4.1(transitive)
- Removedsplit-string@3.1.0(transitive)
- Removedstatic-extend@0.1.2(transitive)
- Removedstring_decoder@1.1.1(transitive)
- Removedto-object-path@0.3.0(transitive)
- Removedto-regex@3.0.2(transitive)
- Removedto-regex-range@2.1.1(transitive)
- Removedunion-value@1.0.1(transitive)
- Removedunset-value@1.0.0(transitive)
- Removedupath@1.2.0(transitive)
- Removedurix@0.1.0(transitive)
- Removeduse@3.1.1(transitive)
- Removedutil-deprecate@1.0.2(transitive)
Updatedanymatch@^3.0.1
Updatedasync-each@^1.0.3
Updatedbraces@^3.0.2
Updatedglob-parent@^5.0.0
Updatedis-binary-path@^2.1.0
Updatedis-glob@^4.0.1
Updatedreaddirp@^3.0.1