Fully refactored the watcher

This commit is contained in:
Jonatan Nilsson 2023-09-03 19:43:59 +00:00
parent b476d23a77
commit c09a4c805e
4 changed files with 315 additions and 664 deletions

View file

@ -1,20 +1,14 @@
import fs from 'fs' import fs from 'fs'
import fsPromise from 'fs/promises'
import path from 'path' import path from 'path'
import util from 'util'
import events from 'events' import events from 'events'
import hasNativeRecursive from './has-native-recursive.mjs' import hasNativeRecursive from './has-native-recursive.mjs'
import * as is from './is.mjs' import * as is from './is.mjs'
const EVENT_UPDATE = 'update'; const EVENT_UPDATE = 'update';
const EVENT_REMOVE = 'remove'; const EVENT_REMOVE = 'remove';
const TYPE_FILE = 'file'
const SKIP_FLAG = Symbol('skip'); const TYPE_DIRECTORY = 'directory'
function hasDup(arr) {
return arr.some(function(v, i, self) {
return self.indexOf(v) !== i;
});
}
function unique(arr) { function unique(arr) {
return arr.filter(function(v, i, self) { return arr.filter(function(v, i, self) {
@ -22,528 +16,274 @@ function unique(arr) {
}); });
} }
// One level flat export default class Watcher extends events.EventEmitter {
function flat1(arr) { constructor(path, options = null, fn = null, { fs: fsoverwrite } = {}) {
return arr.reduce(function(acc, v) { super()
return acc.concat(v);
}, []);
}
function assertEncoding(encoding) { this.ac = new AbortController()
if (encoding && encoding !== 'buffer' && !Buffer.isEncoding(encoding)) { events.setMaxListeners(2000, this.ac.signal)
throw new Error('Unknown encoding: ' + encoding); this._fs = fsoverwrite || fs
this._cache = []
this._cacheTimeout = null
this.listeners = []
this.closed = false
let paths = path
if (is.buffer(paths)) {
paths = paths.toString()
} }
}
function guard(fn) { if (!is.array(paths)) {
if (is.func(fn)) { paths = [paths]
return function(arg, action) {
if (fn(arg, false)) action();
} }
}
if (is.regExp(fn)) {
return function(arg, action) {
if (fn.test(arg)) action();
}
}
return function(arg, action) {
action();
}
}
function composeMessage(names) { paths = unique(paths)
return names.map(function(n) { this.options = options || {}
return is.exists(n) this.fn = fn || null
? [EVENT_UPDATE, n]
: [EVENT_REMOVE, n];
});
}
function getMessages(cache) { if (is.func(this.options)) {
var filtered = unique(cache); this.fn = this.options
this.options = {}
}
// Saving file from an editor? If so, assuming the this._verifyOptions(paths)
// non-existed files in the cache are temporary files }
// generated by an editor and thus be filtered.
var reg = /~$|^\.#|^##$/g;
var hasSpecialChar = cache.some(function(c) {
return reg.test(c);
});
if (hasSpecialChar) { isClosed() {
var dup = hasDup(cache.map(function(c) { return this.closed
return c.replace(reg, ''); }
}));
if (dup) { close() {
filtered = filtered.filter(function(m) { this.closed = true
return is.exists(m); this.ac.abort()
}); this._cache = this.listeners = []
this.emitAsync('close')
}
emitAsync(name, ...args) {
process.nextTick(() => this.emit(name, ...args))
}
_verifyOptions(paths) {
for (let path of paths) {
if (!is.exists(path)) {
this.emitAsync('error', new Error(path + ' does not exist.'))
} }
} }
return composeMessage(filtered); if (this.options.encoding) {
} if (this.options.encoding && this.options.encoding !== 'buffer' && !Buffer.isEncoding(this.options.encoding)) {
throw new Error('Unknown encoding: ' + this.options.encoding);
}
} else {
this.options.encoding = 'utf8'
}
function debounce(info, fn) { if (this.options.delay !== 0 && !this.options.delay) {
var timer, cache = []; this.options.delay = 200
var encoding = info.options.encoding;
var delay = info.options.delay;
if (!is.number(delay)) {
delay = 200;
} }
function handle() {
getMessages(cache).forEach(function(msg) {
msg[1] = Buffer.from(msg[1]);
if (encoding !== 'buffer') {
msg[1] = msg[1].toString(encoding);
}
fn.apply(null, msg);
});
timer = null;
cache = [];
}
return function(rawEvt, name) {
cache.push(name);
if (!timer) {
timer = setTimeout(handle, delay);
}
}
}
function createDupsFilter() { if (is.func(this.fn)) {
var memo = {}; this.on('change', this.fn)
return function(fn) {
return function(evt, name) {
memo[evt + name] = [evt, name];
setTimeout(function() {
Object.keys(memo).forEach(function(n) {
fn.apply(null, memo[n]);
});
memo = {};
});
} }
}
}
function tryWatch(watcher, dir, opts) { if (this.options.manualRecursive !== true) {
hasNativeRecursive(nativeRecursive => {
this.supportsNativeRecursive = nativeRecursive
this.options.manualRecursive = !nativeRecursive
this._startListeners(paths)
})
} else {
this._startListeners(paths)
}
}
_startListeners(paths) {
Promise.all(paths.map(path => this.safeAdd(path)))
.then(
() => this.emit('ready'),
err => this.emit('error', err),
)
}
getWatcherOrNull(name) {
for (let check of this.listeners) {
if (check.path === name) {
return check
}
}
return null
}
shouldInclude(name) {
return this.options.filter
?
(is.func(this.options.filter) && this.options.filter.call(this, name) === true)
|| (is.regExp(this.options.filter) && this.options.filter.test(name))
: true
}
closeWatch(orgItem) {
let item = orgItem
if (typeof item === 'string') {
item = getWatcherOrNull(item)
}
if (!item) {
this.emit('error', new Error(`attempted to close watcher for ${item} but such a watcher could not be found`))
return
}
if (item.watcher) {
item.watcher.close()
}
this._emitEvent(item, EVENT_REMOVE, item.path)
let index = this.listeners.indexOf(item)
if (index < 0) return
this.listeners.splice(index, 1)
}
_emitEvent(item, evt, name) {
if (item.type === TYPE_FILE && !is.samePath(name, item.filename)) return
if (item.type === TYPE_DIRECTORY && !this.shouldInclude(name)) return
if (item.flag) {
item.flag = ''
return
}
let outputName = name
if (this.options.encoding !== 'utf8') {
outputName = Buffer.from(outputName)
if (this.options.encoding !== 'buffer') {
outputName = outputName.toString(this.options.encoding)
}
}
if (!this.options.delay) {
this.emit('change', evt, outputName)
return
}
this._cache.push([evt, name, outputName])
if (this._cacheTimeout) return
this._cacheTimeout = setTimeout(() => {
let cache = this._filterCache(this._cache)
this._cache = []
this._cacheTimeout = null
for (let event of cache) {
try { try {
return fs.watch(dir, opts); this.emit('change', event[0], event[2])
} catch (e) { } catch (err) {
process.nextTick(function() { this.emit('error', err)
watcher.emit('error', e); }
}); }
}, this.options.delay)
} }
}
function getSubDirectories(dir, fn, done = function() {}) { _filterCache(cache) {
if (is.directory(dir)) { let setFound = new Set()
fs.readdir(dir, function(err, all) {
if (err) { let out = cache.reverse().filter(([evt, name]) => {
// don't throw permission errors. if (setFound.has(name)) return false
if (/^(EPERM|EACCES)$/.test(err.code)) { setFound.add(name)
console.warn('Warning: Cannot access %s.', dir); return true
}).reverse()
return out
}
_watcherSink(item, rawEvt, rawName) {
if (this.closed) return
let name = path.join(item.path, rawName || '')
let evt = is.exists(name) ? EVENT_UPDATE : EVENT_REMOVE
if (this.options.recursive && this.options.manualRecursive && item.type === TYPE_DIRECTORY) {
if (evt === EVENT_REMOVE) {
this.closeWatch(name)
return
} else { } else {
throw err; if (is.directory(name)
&& this.getWatcherOrNull(name) === null
&& this.shouldInclude(name) === false) {
this.safeAdd(subItem, TYPE_DIRECTORY)
} }
} }
else {
all.forEach(function(f) {
var sdir = path.join(dir, f);
if (is.directory(sdir)) fn(sdir);
});
done();
} }
});
this._emitEvent(item, evt, name)
}
_pathToItem(name, type) {
if (type === TYPE_FILE) {
let parent = path.join(name, '../')
return {
path: parent,
type: TYPE_FILE,
filename: name,
watcher: null,
flag: '',
}
} else { } else {
done(); return {
} path: name,
} type: TYPE_DIRECTORY,
watcher: null,
function semaphore(final) { flag: '',
var counter = 0;
return function start() {
counter++;
return function stop() {
counter--;
if (counter === 0) final();
};
};
}
function nullCounter() {
return function nullStop() {};
}
function shouldNotSkip(filePath, filter) {
// watch it only if the filter is not function
// or not being skipped explicitly.
return !is.func(filter) || filter(filePath, SKIP_FLAG) !== SKIP_FLAG;
}
var deprecationWarning = util.deprecate(
function() {},
'(node-watch) First param in callback function\
is replaced with event name since 0.5.0, use\
`(evt, filename) => {}` if you want to get the filename'
);
function Watcher() {
events.EventEmitter.call(this);
this.watchers = {};
this._isReady = false;
this._isClosed = false;
}
util.inherits(Watcher, events.EventEmitter);
Watcher.prototype.expose = function() {
var expose = {};
var self = this;
var methods = [
'on', 'emit', 'once',
'close', 'isClosed',
'listeners', 'setMaxListeners', 'getMaxListeners',
'getWatchedPaths'
];
methods.forEach(function(name) {
expose[name] = function() {
return self[name].apply(self, arguments);
}
});
expose.options = self.options
return expose;
}
Watcher.prototype.isClosed = function() {
return this._isClosed;
}
Watcher.prototype.close = function(fullPath) {
var self = this;
if (fullPath) {
var watcher = this.watchers[fullPath];
if (watcher && watcher.close) {
watcher.close();
delete self.watchers[fullPath];
}
getSubDirectories(fullPath, function(fpath) {
self.close(fpath);
});
}
else {
Object.keys(self.watchers).forEach(function(fpath) {
var watcher = self.watchers[fpath];
if (watcher && watcher.close) {
watcher.close();
}
});
this.watchers = {};
}
// Do not close the Watcher unless all child watchers are closed.
// https://github.com/yuanchuan/node-watch/issues/75
if (is.emptyObject(self.watchers)) {
// should emit once
if (!this._isClosed) {
this._isClosed = true;
process.nextTick(emitClose, this);
} }
} }
}
Watcher.prototype.getWatchedPaths = function(fn) {
if (is.func(fn)) {
var self = this;
if (self._isReady) {
fn(Object.keys(self.watchers));
} else {
self.on('ready', function() {
fn(Object.keys(self.watchers));
});
}
}
}
function emitReady(self) {
if (!self._isReady) {
self._isReady = true;
// do not call emit for 'ready' until after watch() has returned,
// so that consumer can call on().
process.nextTick(function () {
self.emit('ready');
});
}
}
function emitClose(self) {
self.emit('close');
}
Watcher.prototype.add = function(watcher, info) {
var self = this;
info = info || { fpath: '' };
var watcherPath = path.resolve(info.fpath);
this.watchers[watcherPath] = watcher;
// Internal callback for handling fs.FSWatcher 'change' events
var internalOnChange = function(rawEvt, rawName) {
if (self.isClosed()) {
return;
} }
// normalise lack of name and convert to full path _watcherError(item, err) {
var name = rawName; if (this.closed) return
if (is.nil(name)) {
name = '';
}
name = path.join(info.fpath, name);
if (info.options.recursive) {
hasNativeRecursive(function(has) {
if (!has) {
var fullPath = path.resolve(name);
// remove watcher on removal
if (!is.exists(name)) {
self.close(fullPath);
}
// watch new created directory
else {
var shouldWatch = is.directory(name)
&& !self.watchers[fullPath]
&& shouldNotSkip(name, info.options.filter);
if (shouldWatch) {
self.watchDirectory(name, info.options);
}
}
}
});
}
handlePublicEvents(rawEvt, name);
};
// Debounced based on the 'delay' option
var handlePublicEvents = debounce(info, function (evt, name) {
// watch single file
if (info.compareName) {
if (info.compareName(name)) {
self.emit('change', evt, name);
}
}
// watch directory
else {
var filterGuard = guard(info.options.filter);
filterGuard(name, function() {
if (self.flag) self.flag = '';
else self.emit('change', evt, name);
});
}
});
watcher.on('error', function(err) {
if (self.isClosed()) {
return;
}
if (is.windows() && err.code === 'EPERM') { if (is.windows() && err.code === 'EPERM') {
watcher.emit('change', EVENT_REMOVE, info.fpath && ''); this.closeWatch(item)
self.flag = 'windows-error'; item.flag = 'windows-error'
self.close(watcherPath);
} else { } else {
self.emit('error', err); self.emit('error', err)
} }
});
watcher.on('change', internalOnChange);
}
Watcher.prototype.watchFile = function(file, options, fn) {
var parent = path.join(file, '../');
var opts = Object.assign({}, options, {
// no filter for single file
filter: null,
encoding: 'utf8'
});
// no need to watch recursively
delete opts.recursive;
var watcher = tryWatch(this, parent, opts);
if (!watcher) {
return;
} }
this.add(watcher, { safeAdd(name, orgType) {
type: 'file', let type = orgType
fpath: parent, if (!type) {
options: Object.assign({}, opts, { type = is.file(name) ? TYPE_FILE : TYPE_DIRECTORY
encoding: options.encoding
}),
compareName: function(n) {
return is.samePath(n, file);
} }
});
if (is.func(fn)) { let item = this._pathToItem(name, type)
if (fn.length === 1) deprecationWarning(); let options = {
this.on('change', fn); encoding: 'utf8',
signal: this.ac.signal,
}
if (!this.options.manualRecursive && item.type !== TYPE_FILE && this.options.recursive) {
options.recursive = true
}
try {
item.watcher = this._fs.watch(item.path, options)
} catch (err) {
this.emitAsync('error', err)
}
if (!item.watcher) return
this.listeners.push(item)
item.watcher.on('error', this._watcherError.bind(this, item))
item.watcher.on('change', this._watcherSink.bind(this, item))
if (options.recursive || item.type === TYPE_FILE) return
return fsPromise.readdir(item.path, { withFileTypes: true })
.then(directories => directories.filter(dir => dir.isDirectory()))
.then(directories => {
return Promise.all(directories.map(dir => this.safeAdd(path.join(item.path, dir.name), TYPE_DIRECTORY)))
})
} }
} }
Watcher.prototype.updateDelay = function(delay) {
console.log()
}
Watcher.prototype.watchDirectory = function(dir, options, fn, counter = nullCounter) {
var self = this;
var done = counter();
hasNativeRecursive(function(has) {
// always specify recursive
options.recursive = !!options.recursive;
// using utf8 internally
var opts = Object.assign({}, options, {
encoding: 'utf8'
});
if (!has) {
delete opts.recursive;
}
// check if it's closed before calling watch.
if (self._isClosed) {
done();
return self.close();
}
var watcher = tryWatch(self, dir, opts);
if (!watcher) {
done();
return;
}
self.add(watcher, {
type: 'dir',
fpath: dir,
options: options
});
if (is.func(fn)) {
if (fn.length === 1) deprecationWarning();
self.on('change', fn);
}
if (options.recursive && !has) {
getSubDirectories(dir, function(d) {
if (shouldNotSkip(d, options.filter)) {
self.watchDirectory(d, options, null, counter);
}
}, counter());
}
done();
});
}
function composeWatcher(watchers) {
var watcher = new Watcher();
var filterDups = createDupsFilter();
var counter = watchers.length;
watchers.forEach(function(w) {
w.on('change', filterDups(function(evt, name) {
watcher.emit('change', evt, name);
}));
w.on('error', function(err) {
watcher.emit('error', err);
});
w.on('ready', function() {
if (!(--counter)) {
emitReady(watcher);
}
});
});
watcher.close = function() {
watchers.forEach(function(w) {
w.close();
});
process.nextTick(emitClose, watcher);
}
watcher.getWatchedPaths = function(fn) {
if (is.func(fn)) {
var promises = watchers.map(function(w) {
return new Promise(function(resolve) {
w.getWatchedPaths(resolve);
});
});
Promise.all(promises).then(function(result) {
var ret = unique(flat1(result));
fn(ret);
});
}
}
return watcher.expose();
}
export default function watch(fpath, options, fn) {
var watcher = new Watcher();
if (is.buffer(fpath)) {
fpath = fpath.toString();
}
if (!is.array(fpath) && !is.exists(fpath)) {
process.nextTick(function() {
watcher.emit('error',
new Error(fpath + ' does not exist.')
);
});
}
if (is.string(options)) {
throw new Error(`Invalid option, encoding as string is no longer supported. Use { encoding: "${options}" } instead.`)
}
if (is.func(options)) {
fn = options;
options = {};
}
if (arguments.length < 2) {
options = {};
}
if (options.encoding) {
assertEncoding(options.encoding);
} else {
options.encoding = 'utf8';
}
if (is.array(fpath)) {
if (fpath.length === 1) {
return watch(fpath[0], options, fn);
}
var filterDups = createDupsFilter();
return composeWatcher(unique(fpath).map(function(f) {
var w = watch(f, options);
if (is.func(fn)) {
w.on('change', filterDups(fn));
}
return w;
}));
}
if (is.file(fpath)) {
watcher.watchFile(fpath, options, fn);
emitReady(watcher);
}
else if (is.directory(fpath)) {
var counter = semaphore(function () {
emitReady(watcher);
});
watcher.watchDirectory(fpath, options, fn, counter);
}
return watcher.expose();
}

View file

@ -185,7 +185,6 @@ t.describe('CLI', function() {
cli.targets = ['test/testtree/*/*.mjs'] cli.targets = ['test/testtree/*/*.mjs']
await cli.processTargets() await cli.processTargets()
console.log(cli.files)
assert.strictEqual(cli.files.length, 4) assert.strictEqual(cli.files.length, 4)
cli.files.sort() cli.files.sort()
assert.deepEqual(cli.files, [ assert.deepEqual(cli.files, [

View file

@ -1,10 +1,9 @@
import path from 'path' import path from 'path'
import fs from 'fs'
import assert from '../lib/assert.mjs' import assert from '../lib/assert.mjs'
import t from '../lib/eltro.mjs' import t from '../lib/eltro.mjs'
import { Builder, Counter } from './watch/builder.mjs' import { Builder, Counter } from './watch/builder.mjs'
import watch from '../lib/watch/index.mjs' import Watcher from '../lib/watch/index.mjs'
import * as is from '../lib/watch/is.mjs'
import hasNativeRecursive from '../lib/watch/has-native-recursive.mjs'
const builder = new Builder() const builder = new Builder()
let watcher let watcher
@ -32,27 +31,12 @@ t.after(function() {
} }
}) })
function wait(fn, timeout) { t.describe('watcher', function() {
try {
fn()
} catch (error) {
timeout -= 30
if (timeout >= 0) {
setTimeout(function() {
wait(fn, timeout)
}, 30)
} else {
throw error
}
}
}
t.only().describe('watcher', function() {
t.describe('process events', function() { t.describe('process events', function() {
t.test('should emit `close` event', function(done) { t.test('should emit `close` event', function(done) {
var file = 'home/a/file1' var file = 'home/a/file1'
var fpath = builder.getPath(file) var fpath = builder.getPath(file)
watcher = watch(fpath, function() {}) watcher = new Watcher(fpath, function() {})
watcher.on('close', done) watcher.on('close', done)
watcher.close() watcher.close()
}) })
@ -60,13 +44,13 @@ t.only().describe('watcher', function() {
t.test('should emit `ready` event when watching a file', function(done) { t.test('should emit `ready` event when watching a file', function(done) {
var file = 'home/a/file1' var file = 'home/a/file1'
var fpath = builder.getPath(file) var fpath = builder.getPath(file)
watcher = watch(fpath) watcher = new Watcher(fpath)
watcher.on('ready', done) watcher.on('ready', done)
}) })
t.test('should emit `ready` event when watching a directory recursively', function(done) { t.test('should emit `ready` event when watching a directory recursively', function(done) {
var dir = builder.getPath('home') var dir = builder.getPath('home')
watcher = watch(dir, { recursive: true }) watcher = new Watcher(dir, { recursive: true })
watcher.on('ready', done) watcher.on('ready', done)
}) })
@ -74,7 +58,7 @@ t.only().describe('watcher', function() {
var dir1 = builder.getPath('home/a') var dir1 = builder.getPath('home/a')
var dir2 = builder.getPath('home/b') var dir2 = builder.getPath('home/b')
var file = builder.getPath('home/b/file1') var file = builder.getPath('home/b/file1')
watcher = watch([dir1, dir2, file], { recursive: true }) watcher = new Watcher([dir1, dir2, file], { recursive: true })
watcher.on('ready', done) watcher.on('ready', done)
}) })
}) })
@ -84,7 +68,7 @@ t.only().describe('watcher', function() {
var counter = new Counter(done, 3) var counter = new Counter(done, 3)
var file = 'home/a/file1' var file = 'home/a/file1'
var fpath = builder.getPath(file) var fpath = builder.getPath(file)
watcher = watch(fpath, { delay: 0 }, function(evt, name) { watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
assert.strictEqual(fpath, name) assert.strictEqual(fpath, name)
counter.count() counter.count()
}) })
@ -105,7 +89,7 @@ t.only().describe('watcher', function() {
builder.newFile('home/a/file1'), builder.newFile('home/a/file1'),
builder.newFile('home/a/file2'), builder.newFile('home/a/file2'),
]).then(() => { ]).then(() => {
watcher = watch(fpath, { delay: 0 }, function(evt, name) { watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
set.delete(name) set.delete(name)
if (!set.size) { if (!set.size) {
done() done()
@ -130,7 +114,7 @@ t.only().describe('watcher', function() {
var middle = start var middle = start
var end = start var end = start
watcher = watch(fpath, { delay: 100 }, function(evt, name) { watcher = new Watcher(fpath, { delay: 100 }, function(evt, name) {
if (fpath === name) counter.count() if (fpath === name) counter.count()
}) })
@ -165,7 +149,7 @@ t.only().describe('watcher', function() {
]) ])
var changes = [] var changes = []
var extra = [] var extra = []
watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (set.has(name)) { if (set.has(name)) {
changes.push(name) changes.push(name)
counter.count() counter.count()
@ -189,16 +173,14 @@ t.only().describe('watcher', function() {
var fpath = builder.getPath('home/a/file1') var fpath = builder.getPath('home/a/file1')
builder.newFile('home/a/file1') builder.newFile('home/a/file1')
.then(() => { .then(() => {
watcher = watch(fpath, Object.defineProperty({}, 'test', { watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) {
enumerable: true,
get: function() {
builder.removeSync('home/a') builder.removeSync('home/a')
return 'test' return fs.watch(path, options)
} } } })
watcher.on('error', done.finish(function(err) {
assert.deepStrictEqual(watcher.listeners, [])
})) }))
watcher.on('error', function() {
done()
})
}) })
}) })
}) })
@ -209,7 +191,7 @@ t.only().describe('watcher', function() {
var dir = builder.getPath('home/c') var dir = builder.getPath('home/c')
builder.createDirectory('home/c').then(() => { builder.createDirectory('home/c').then(() => {
watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (name === dir && evt === 'remove') { if (name === dir && evt === 'remove') {
done() done()
} }
@ -224,7 +206,7 @@ t.only().describe('watcher', function() {
var home = builder.getPath('home') var home = builder.getPath('home')
builder.remove('home/new').then(() => { builder.remove('home/new').then(() => {
watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (name === builder.getPath('home/new/file1')) { if (name === builder.getPath('home/new/file1')) {
done() done()
} }
@ -244,20 +226,21 @@ t.only().describe('watcher', function() {
var options = { var options = {
delay: 0, delay: 0,
recursive: true, recursive: true,
filter: function(filePath, SKIP_FLAG) { filter: function(filePath) {
if (/ignored/.test(filePath)) { if (/ignored/.test(filePath)) {
counter.count() counter.count()
return SKIP_FLAG
}
return false return false
} }
return true
}
} }
builder.remove('home/ignored/file').then(() => { builder.remove('home/ignored/file').then(() => {
watcher = watch(home, options, function(evt, name) { watcher = new Watcher(home, options, function(evt, name) {
assert.fail("should not watch new created directories which are being skipped in the filter event detect: " + name) assert.fail("should not watch new created directories which are being skipped in the filter event detect: " + name)
}) })
watcher.on('ready', function() { watcher.on('ready', function() {
builder.newFile('home/ignored/file') builder.newFile('home/ignored/file')
.catch(done) .catch(done)
@ -279,7 +262,7 @@ t.only().describe('watcher', function() {
builder.newFile('home/e/file2'), builder.newFile('home/e/file2'),
]).then(() => { ]).then(() => {
watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (name === dir || name === file1 || name === file2) { if (name === dir || name === file1 || name === file2) {
if (!set.has(name)) { if (!set.has(name)) {
set.add(name) set.add(name)
@ -301,7 +284,7 @@ t.only().describe('watcher', function() {
var home = builder.getPath('home') var home = builder.getPath('home')
builder.remove('home/new').then(() => { builder.remove('home/new').then(() => {
watcher = watch(home, { delay: 200, recursive: true }, function(evt, name) { watcher = new Watcher(home, { delay: 200, recursive: true }, function(evt, name) {
if (name === builder.getPath('home/new/file1')) { if (name === builder.getPath('home/new/file1')) {
counter.count() counter.count()
} }
@ -319,16 +302,14 @@ t.only().describe('watcher', function() {
var fpath = builder.getPath(dir) var fpath = builder.getPath(dir)
builder.createDirectory(dir).then(() => { builder.createDirectory(dir).then(() => {
watcher = watch(fpath, Object.defineProperty({}, 'test', { watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) {
enumerable: true,
get: function() {
builder.removeSync(dir) builder.removeSync(dir)
return 'test' return fs.watch(path, options)
} } } })
watcher.on('error', done.finish(function(err) {
assert.deepStrictEqual(watcher.listeners, [])
})) }))
watcher.on('error', function() {
done()
})
}) })
}) })
}) })
@ -342,7 +323,7 @@ t.only().describe('watcher', function() {
}) })
t.test('should identify `remove` event', function(done) { t.test('should identify `remove` event', function(done) {
watcher = watch(fpath, { delay: 0 }, function(evt, name) { watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
if (evt === 'remove' && name === fpath) { if (evt === 'remove' && name === fpath) {
done() done()
} }
@ -357,7 +338,7 @@ t.only().describe('watcher', function() {
var home = builder.getPath('home') var home = builder.getPath('home')
var fpath = builder.getPath(dir) var fpath = builder.getPath(dir)
watcher = watch(home, { delay: 0 }, function(evt, name) { watcher = new Watcher(home, { delay: 0 }, function(evt, name) {
if (evt === 'remove' && name === fpath) done() if (evt === 'remove' && name === fpath) done()
}) })
watcher.on('ready', function() { watcher.on('ready', function() {
@ -372,7 +353,7 @@ t.only().describe('watcher', function() {
builder.newRandomFiles(dir, 100).then(names => { builder.newRandomFiles(dir, 100).then(names => {
var counter = new Counter(done, names.length) var counter = new Counter(done, names.length)
watcher = watch(fpath, { delay: 10 }, function(evt, name) { watcher = new Watcher(fpath, { delay: 10 }, function(evt, name) {
if (evt === 'remove') counter.count() if (evt === 'remove') counter.count()
}) })
@ -386,7 +367,7 @@ t.only().describe('watcher', function() {
var file = 'home/b/file1' var file = 'home/b/file1'
var fpath = builder.getPath(file) var fpath = builder.getPath(file)
builder.newFile(file).then(() => { builder.newFile(file).then(() => {
watcher = watch(fpath, { delay: 0 }, function(evt, name) { watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
if (evt === 'update' && name === fpath) done() if (evt === 'update' && name === fpath) done()
}) })
watcher.on('ready', function() { watcher.on('ready', function() {
@ -400,7 +381,7 @@ t.only().describe('watcher', function() {
var file = 'home/a/newfile_' + builder.randomName() var file = 'home/a/newfile_' + builder.randomName()
var fpath = builder.getPath(file) var fpath = builder.getPath(file)
watcher = watch(dir, { delay: 0 }, function(evt, name) { watcher = new Watcher(dir, { delay: 0 }, function(evt, name) {
if (evt === 'update' && name === fpath) done() if (evt === 'update' && name === fpath) done()
}) })
watcher.on('ready', function() { watcher.on('ready', function() {
@ -414,7 +395,7 @@ t.only().describe('watcher', function() {
t.test('should watch recursively with `recursive: true` option', function(done) { t.test('should watch recursively with `recursive: true` option', function(done) {
var dir = builder.getPath('home') var dir = builder.getPath('home')
var file = builder.getPath('home/bb/file1') var file = builder.getPath('home/bb/file1')
watcher = watch(dir, { delay: 0, recursive: true }, function(evt, name) { watcher = new Watcher(dir, { delay: 0, recursive: true }, function(evt, name) {
if (file === name) { if (file === name) {
done() done()
} }
@ -442,7 +423,7 @@ t.only().describe('watcher', function() {
t.test('should throw on invalid encoding', function(done) { t.test('should throw on invalid encoding', function(done) {
options.encoding = 'unknown' options.encoding = 'unknown'
try { try {
watcher = watch(fdir, options) watcher = new Watcher(fdir, options)
} catch (e) { } catch (e) {
done() done()
} }
@ -450,7 +431,7 @@ t.only().describe('watcher', function() {
t.test('should accept an encoding string', function(done) { t.test('should accept an encoding string', function(done) {
options.encoding = 'utf8' options.encoding = 'utf8'
watcher = watch(fdir, options, done.finish(function(evt, name) { watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.strictEqual(name.toString(), fpath) assert.strictEqual(name.toString(), fpath)
})) }))
watcher.on('ready', function() { watcher.on('ready', function() {
@ -460,7 +441,7 @@ t.only().describe('watcher', function() {
t.test('should support buffer encoding', function(done) { t.test('should support buffer encoding', function(done) {
options.encoding = 'buffer' options.encoding = 'buffer'
watcher = watch(fdir, options, done.finish(function(evt, name) { watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.ok(Buffer.isBuffer(name), 'not a Buffer') assert.ok(Buffer.isBuffer(name), 'not a Buffer')
assert.strictEqual(name.toString(), fpath) assert.strictEqual(name.toString(), fpath)
})) }))
@ -472,7 +453,7 @@ t.only().describe('watcher', function() {
t.test('should support base64 encoding', function(done) { t.test('should support base64 encoding', function(done) {
options.encoding = 'base64' options.encoding = 'base64'
watcher = watch(fdir, options, done.finish(function(evt, name) { watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.strictEqual( assert.strictEqual(
name, name,
Buffer.from(fpath).toString('base64'), Buffer.from(fpath).toString('base64'),
@ -486,7 +467,7 @@ t.only().describe('watcher', function() {
t.test('should support hex encoding', function(done) { t.test('should support hex encoding', function(done) {
options.encoding = 'hex' options.encoding = 'hex'
watcher = watch(fdir, options, done.finish(function(evt, name) { watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.strictEqual( assert.strictEqual(
name, name,
Buffer.from(fpath).toString('hex'), Buffer.from(fpath).toString('hex'),
@ -516,7 +497,7 @@ t.only().describe('watcher', function() {
} }
} }
watcher = watch(builder.getPath('home'), options, function(evt, name) { watcher = new Watcher(builder.getPath('home'), options, function(evt, name) {
if (/deep_node_modules/.test(name)) { if (/deep_node_modules/.test(name)) {
matchIgnoredDir = true matchIgnoredDir = true
} else { } else {
@ -552,7 +533,7 @@ t.only().describe('watcher', function() {
var times = 0 var times = 0
var matchIgnoredFile = false var matchIgnoredFile = false
watcher = watch(dir, options, function(evt, name) { watcher = new Watcher(dir, options, function(evt, name) {
if (name === builder.getPath(file1)) { if (name === builder.getPath(file1)) {
matchIgnoredFile = true matchIgnoredFile = true
} }
@ -585,7 +566,7 @@ t.only().describe('watcher', function() {
var times = 0 var times = 0
var matchIgnoredFile = false var matchIgnoredFile = false
watcher = watch(dir, options, function(evt, name) { watcher = new Watcher(dir, options, function(evt, name) {
if (name === builder.getPath(file1)) { if (name === builder.getPath(file1)) {
matchIgnoredFile = true matchIgnoredFile = true
} }
@ -605,31 +586,21 @@ t.only().describe('watcher', function() {
var options = { var options = {
delay: 0, delay: 0,
recursive: true, recursive: true,
manualRecursive: true,
filter: function(name, skip) { filter: function(name, skip) {
if (/\/deep_node_modules/.test(name)) return skip if (/\/deep_node_modules/.test(name)) return skip
} }
} }
watcher = watch(home, options) watcher = new Watcher(home, options)
watcher.getWatchedPaths(function(paths) { watcher.on('ready', done.finish(function() {
hasNativeRecursive(function(supportRecursive) { let homeFiltered = builder.getAllDirectories().filter(function(name) {
var watched = supportRecursive
// The skip flag has no effect to the platforms which support recursive option,
// so the home directory is the only one that's in the watching list.
? [home]
// The deep_node_modules and all its subdirectories should not be watched
// with skip flag specified in the filter.
: builder.getAllDirectories().filter(function(name) {
return !/\/deep_node_modules/.test(name) return !/\/deep_node_modules/.test(name)
}) }).sort()
let watchersPaths = watcher.listeners.map(x => x.path).sort()
assert.deepStrictEqual( assert.deepStrictEqual(watchersPaths, homeFiltered)
watched.sort(), paths.sort() }))
)
done()
})
})
}) })
}) })
}) })
@ -637,7 +608,7 @@ t.only().describe('watcher', function() {
t.describe('parameters', function() { t.describe('parameters', function() {
t.test('should throw error on non-existed file', function(done) { t.test('should throw error on non-existed file', function(done) {
var somedir = builder.getPath('home/somedir') var somedir = builder.getPath('home/somedir')
watcher = watch(somedir) watcher = new Watcher(somedir)
watcher.on('error', done.finish(function(err) { watcher.on('error', done.finish(function(err) {
assert.match(err.message, /not exist/) assert.match(err.message, /not exist/)
})) }))
@ -645,7 +616,7 @@ t.only().describe('watcher', function() {
t.test('should accept filename as Buffer', function(done) { t.test('should accept filename as Buffer', function(done) {
var fpath = builder.getPath('home/a/file1') var fpath = builder.getPath('home/a/file1')
watcher = watch(Buffer.from(fpath), { delay: 0 }, done.finish(function(evt, name) { watcher = new Watcher(Buffer.from(fpath), { delay: 0 }, done.finish(function(evt, name) {
assert.strictEqual(name, fpath) assert.strictEqual(name, fpath)
})) }))
watcher.on('ready', function() { watcher.on('ready', function() {
@ -667,7 +638,7 @@ t.only().describe('watcher', function() {
builder.newFile(file1), builder.newFile(file1),
builder.newFile(file2), builder.newFile(file2),
]).then(function() { ]).then(function() {
watcher = watch(fpaths, { delay: 0 }, function(evt, name) { watcher = new Watcher(fpaths, { delay: 0 }, function(evt, name) {
if (set.has(name)) { if (set.has(name)) {
set.delete(name) set.delete(name)
counter.count() counter.count()
@ -696,9 +667,8 @@ t.only().describe('watcher', function() {
builder.getPath(file2) builder.getPath(file2)
] ]
console.log()
var changes = [] var changes = []
watcher = watch(fpaths, { delay: 100, recursive: true }, function(evt, name) { watcher = new Watcher(fpaths, { delay: 100, recursive: true }, function(evt, name) {
changes.push(name) changes.push(name)
counter.count() counter.count()
}) })
@ -728,7 +698,7 @@ t.only().describe('watcher', function() {
var file = 'home/a/file1' var file = 'home/a/file1'
var fpath = builder.getPath(file) var fpath = builder.getPath(file)
watcher = watch(dir, { delay: 0 }) watcher = new Watcher(dir, { delay: 0 })
watcher.on('change', done.finish(function(evt, name) { watcher.on('change', done.finish(function(evt, name) {
assert.strictEqual(evt, 'update') assert.strictEqual(evt, 'update')
@ -736,7 +706,7 @@ t.only().describe('watcher', function() {
})) }))
watcher.on('ready', done.wrap(function() { watcher.on('ready', done.wrap(function() {
builder.modify(file) builder.modify(file).catch(done)
})) }))
}) })
@ -745,7 +715,7 @@ t.only().describe('watcher', function() {
var dir = builder.getPath('home/a') var dir = builder.getPath('home/a')
var file = 'home/a/file1' var file = 'home/a/file1'
var times = 0 var times = 0
watcher = watch(dir, { delay: 0 }) watcher = new Watcher(dir, { delay: 0 })
watcher.on('change', function(evt, name) { watcher.on('change', function(evt, name) {
times++ times++
}) })
@ -766,76 +736,16 @@ t.only().describe('watcher', function() {
t.test('should not watch after .close() is called', function(done) { t.test('should not watch after .close() is called', function(done) {
var dir = builder.getPath('home') var dir = builder.getPath('home')
watcher = watch(dir, { delay: 0, recursive: true }) watcher = new Watcher(dir, { delay: 0, recursive: true })
watcher.on('ready', function() {
watcher.close() watcher.close()
})
watcher.getWatchedPaths(done.finish(function(dirs) { watcher.on('close', done.finish(function(dirs) {
assert.strictEqual(dirs.length, 0) assert.strictEqual(watcher.listeners.length, 0)
})) }))
}) })
}) })
t.describe('getWatchedPaths()', function() {
t.test('should get all the watched paths', function(done) {
var home = builder.getPath('home')
watcher = watch(home, { delay: 0, recursive: true })
watcher.getWatchedPaths(function(paths) {
hasNativeRecursive(done.finish(function(supportRecursive) {
var watched = supportRecursive
// The home directory is the only one that's being watched
// if the recursive option is natively supported.
? [home]
// Otherwise it should include all its subdirectories.
: builder.getAllDirectories()
assert.deepStrictEqual(
watched.sort(), paths.sort()
)
}))
})
})
t.test('should get its parent path instead of the file itself', function(done) {
var file = builder.getPath('home/a/file1')
// The parent path is actually being watched instead.
var parent = builder.getPath('home/a')
watcher = watch(file, { delay: 0 })
watcher.getWatchedPaths(done.finish(function(paths) {
assert.deepStrictEqual([parent], paths)
}))
})
t.test('should work correctly with composed watcher', function(done) {
var a = builder.getPath('home/a')
var b = builder.getPath('home/b')
var file = builder.getPath('home/b/file1')
var nested = builder.getPath('home/deep_node_modules')
var ma = builder.getPath('home/deep_node_modules/ma')
var mb = builder.getPath('home/deep_node_modules/mb')
var mc = builder.getPath('home/deep_node_modules/mc')
watcher = watch([a, file, nested], {
delay: 0,
recursive: true
})
watcher.getWatchedPaths(function(paths) {
hasNativeRecursive(done.finish(function(supportRecursive) {
var watched = supportRecursive
? [a, b, nested]
: [a, b, nested, ma, mb, mc]
assert.deepStrictEqual(
watched.sort(), paths.sort()
)
}))
})
})
})
}) })
}) })

View file

@ -135,6 +135,8 @@ Builder.prototype.modify = function(fpath, delay) {
Builder.prototype.remove = function(fpath) { Builder.prototype.remove = function(fpath) {
let filePath = this.getPath(fpath) let filePath = this.getPath(fpath)
return fs.rm(filePath, { recursive: true, force: true }) return fs.rm(filePath, { recursive: true, force: true })
.catch(() => this.delay(100))
.then(() => fs.rm(filePath, { recursive: true, force: true }))
} }
Builder.prototype.removeSync = function(fpath) { Builder.prototype.removeSync = function(fpath) {