diff --git a/lib/watch/index.mjs b/lib/watch/index.mjs index 1779dd3..645d8f5 100644 --- a/lib/watch/index.mjs +++ b/lib/watch/index.mjs @@ -1,20 +1,14 @@ import fs from 'fs' +import fsPromise from 'fs/promises' import path from 'path' -import util from 'util' import events from 'events' import hasNativeRecursive from './has-native-recursive.mjs' import * as is from './is.mjs' const EVENT_UPDATE = 'update'; const EVENT_REMOVE = 'remove'; - -const SKIP_FLAG = Symbol('skip'); - -function hasDup(arr) { - return arr.some(function(v, i, self) { - return self.indexOf(v) !== i; - }); -} +const TYPE_FILE = 'file' +const TYPE_DIRECTORY = 'directory' function unique(arr) { return arr.filter(function(v, i, self) { @@ -22,528 +16,274 @@ function unique(arr) { }); } -// One level flat -function flat1(arr) { - return arr.reduce(function(acc, v) { - return acc.concat(v); - }, []); -} +export default class Watcher extends events.EventEmitter { + constructor(path, options = null, fn = null, { fs: fsoverwrite } = {}) { + super() -function assertEncoding(encoding) { - if (encoding && encoding !== 'buffer' && !Buffer.isEncoding(encoding)) { - throw new Error('Unknown encoding: ' + encoding); - } -} - -function guard(fn) { - if (is.func(fn)) { - return function(arg, action) { - if (fn(arg, false)) action(); + this.ac = new AbortController() + events.setMaxListeners(2000, this.ac.signal) + this._fs = fsoverwrite || fs + this._cache = [] + this._cacheTimeout = null + this.listeners = [] + this.closed = false + let paths = path + if (is.buffer(paths)) { + paths = paths.toString() } - } - if (is.regExp(fn)) { - return function(arg, action) { - if (fn.test(arg)) action(); + + if (!is.array(paths)) { + paths = [paths] } - } - return function(arg, action) { - action(); - } -} -function composeMessage(names) { - return names.map(function(n) { - return is.exists(n) - ? [EVENT_UPDATE, n] - : [EVENT_REMOVE, n]; - }); -} + paths = unique(paths) + this.options = options || {} + this.fn = fn || null -function getMessages(cache) { - var filtered = unique(cache); - - // Saving file from an editor? If so, assuming the - // 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) { - var dup = hasDup(cache.map(function(c) { - return c.replace(reg, ''); - })); - if (dup) { - filtered = filtered.filter(function(m) { - return is.exists(m); - }); + if (is.func(this.options)) { + this.fn = this.options + this.options = {} } + + this._verifyOptions(paths) } - return composeMessage(filtered); -} - -function debounce(info, fn) { - var timer, cache = []; - var encoding = info.options.encoding; - var delay = info.options.delay; - if (!is.number(delay)) { - delay = 200; + isClosed() { + return this.closed } - function handle() { - getMessages(cache).forEach(function(msg) { - msg[1] = Buffer.from(msg[1]); - if (encoding !== 'buffer') { - msg[1] = msg[1].toString(encoding); + + close() { + this.closed = true + 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.')) } - fn.apply(null, msg); - }); - timer = null; - cache = []; - } - return function(rawEvt, name) { - cache.push(name); - if (!timer) { - timer = setTimeout(handle, delay); } - } -} -function createDupsFilter() { - var memo = {}; - 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) { - try { - return fs.watch(dir, opts); - } catch (e) { - process.nextTick(function() { - watcher.emit('error', e); - }); - } -} - -function getSubDirectories(dir, fn, done = function() {}) { - if (is.directory(dir)) { - fs.readdir(dir, function(err, all) { - if (err) { - // don't throw permission errors. - if (/^(EPERM|EACCES)$/.test(err.code)) { - console.warn('Warning: Cannot access %s.', dir); - } else { - throw err; - } + 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 { - all.forEach(function(f) { - var sdir = path.join(dir, f); - if (is.directory(sdir)) fn(sdir); - }); - done(); - } - }); - } else { - done(); - } -} - -function semaphore(final) { - 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)); - }); + this.options.encoding = 'utf8' + } + + if (this.options.delay !== 0 && !this.options.delay) { + this.options.delay = 200 + } + + if (is.func(this.fn)) { + this.on('change', this.fn) + } + + if (this.options.manualRecursive !== true) { + hasNativeRecursive(nativeRecursive => { + this.supportsNativeRecursive = nativeRecursive + this.options.manualRecursive = !nativeRecursive + this._startListeners(paths) + }) + } else { + this._startListeners(paths) } } -} -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'); - }); + _startListeners(paths) { + Promise.all(paths.map(path => this.safeAdd(path))) + .then( + () => this.emit('ready'), + err => this.emit('error', err), + ) } -} -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 - var name = rawName; - 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); + getWatcherOrNull(name) { + for (let check of this.listeners) { + if (check.path === name) { + return check } } - // watch directory - else { - var filterGuard = guard(info.options.filter); - filterGuard(name, function() { - if (self.flag) self.flag = ''; - else self.emit('change', evt, name); - }); - } - }); + return null + } - watcher.on('error', function(err) { - if (self.isClosed()) { - return; + 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 { + this.emit('change', event[0], event[2]) + } catch (err) { + this.emit('error', err) + } + } + }, this.options.delay) + } + + _filterCache(cache) { + let setFound = new Set() + + let out = cache.reverse().filter(([evt, name]) => { + if (setFound.has(name)) return false + setFound.add(name) + 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 { + if (is.directory(name) + && this.getWatcherOrNull(name) === null + && this.shouldInclude(name) === false) { + this.safeAdd(subItem, TYPE_DIRECTORY) + } + } + } + + 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 { + return { + path: name, + type: TYPE_DIRECTORY, + watcher: null, + flag: '', + } + } + } + + _watcherError(item, err) { + if (this.closed) return + if (is.windows() && err.code === 'EPERM') { - watcher.emit('change', EVENT_REMOVE, info.fpath && ''); - self.flag = 'windows-error'; - self.close(watcherPath); + this.closeWatch(item) + item.flag = 'windows-error' } 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, { - type: 'file', - fpath: parent, - options: Object.assign({}, opts, { - encoding: options.encoding - }), - compareName: function(n) { - return is.samePath(n, file); + safeAdd(name, orgType) { + let type = orgType + if (!type) { + type = is.file(name) ? TYPE_FILE : TYPE_DIRECTORY } - }); - if (is.func(fn)) { - if (fn.length === 1) deprecationWarning(); - this.on('change', fn); + let item = this._pathToItem(name, type) + let options = { + 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(); -} diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 38f745e..fa52e3e 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -185,7 +185,6 @@ t.describe('CLI', function() { cli.targets = ['test/testtree/*/*.mjs'] await cli.processTargets() - console.log(cli.files) assert.strictEqual(cli.files.length, 4) cli.files.sort() assert.deepEqual(cli.files, [ diff --git a/test/watch.test.mjs b/test/watch.test.mjs index 41c8486..85877ca 100644 --- a/test/watch.test.mjs +++ b/test/watch.test.mjs @@ -1,10 +1,9 @@ import path from 'path' +import fs from 'fs' import assert from '../lib/assert.mjs' import t from '../lib/eltro.mjs' import { Builder, Counter } from './watch/builder.mjs' -import watch from '../lib/watch/index.mjs' -import * as is from '../lib/watch/is.mjs' -import hasNativeRecursive from '../lib/watch/has-native-recursive.mjs' +import Watcher from '../lib/watch/index.mjs' const builder = new Builder() let watcher @@ -32,27 +31,12 @@ t.after(function() { } }) -function wait(fn, timeout) { - 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('watcher', function() { t.describe('process events', function() { t.test('should emit `close` event', function(done) { var file = 'home/a/file1' var fpath = builder.getPath(file) - watcher = watch(fpath, function() {}) + watcher = new Watcher(fpath, function() {}) watcher.on('close', done) watcher.close() }) @@ -60,13 +44,13 @@ t.only().describe('watcher', function() { t.test('should emit `ready` event when watching a file', function(done) { var file = 'home/a/file1' var fpath = builder.getPath(file) - watcher = watch(fpath) + watcher = new Watcher(fpath) watcher.on('ready', done) }) t.test('should emit `ready` event when watching a directory recursively', function(done) { var dir = builder.getPath('home') - watcher = watch(dir, { recursive: true }) + watcher = new Watcher(dir, { recursive: true }) watcher.on('ready', done) }) @@ -74,7 +58,7 @@ t.only().describe('watcher', function() { var dir1 = builder.getPath('home/a') var dir2 = builder.getPath('home/b') 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) }) }) @@ -84,7 +68,7 @@ t.only().describe('watcher', function() { var counter = new Counter(done, 3) var file = 'home/a/file1' 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) counter.count() }) @@ -105,7 +89,7 @@ t.only().describe('watcher', function() { builder.newFile('home/a/file1'), builder.newFile('home/a/file2'), ]).then(() => { - watcher = watch(fpath, { delay: 0 }, function(evt, name) { + watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) { set.delete(name) if (!set.size) { done() @@ -130,7 +114,7 @@ t.only().describe('watcher', function() { var middle = 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() }) @@ -165,7 +149,7 @@ t.only().describe('watcher', function() { ]) var changes = [] 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)) { changes.push(name) counter.count() @@ -189,16 +173,14 @@ t.only().describe('watcher', function() { var fpath = builder.getPath('home/a/file1') builder.newFile('home/a/file1') .then(() => { - watcher = watch(fpath, Object.defineProperty({}, 'test', { - enumerable: true, - get: function() { - builder.removeSync('home/a') - return 'test' - } + watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) { + builder.removeSync('home/a') + 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') 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') { done() } @@ -224,7 +206,7 @@ t.only().describe('watcher', function() { var home = builder.getPath('home') 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')) { done() } @@ -244,19 +226,20 @@ t.only().describe('watcher', function() { var options = { delay: 0, recursive: true, - filter: function(filePath, SKIP_FLAG) { + filter: function(filePath) { if (/ignored/.test(filePath)) { counter.count() - return SKIP_FLAG + return false } - return false + return true } } 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) }) + watcher.on('ready', function() { builder.newFile('home/ignored/file') @@ -279,7 +262,7 @@ t.only().describe('watcher', function() { builder.newFile('home/e/file2'), ]).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 (!set.has(name)) { set.add(name) @@ -301,7 +284,7 @@ t.only().describe('watcher', function() { var home = builder.getPath('home') 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')) { counter.count() } @@ -319,16 +302,14 @@ t.only().describe('watcher', function() { var fpath = builder.getPath(dir) builder.createDirectory(dir).then(() => { - watcher = watch(fpath, Object.defineProperty({}, 'test', { - enumerable: true, - get: function() { - builder.removeSync(dir) - return 'test' - } + watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) { + builder.removeSync(dir) + 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) { - watcher = watch(fpath, { delay: 0 }, function(evt, name) { + watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) { if (evt === 'remove' && name === fpath) { done() } @@ -357,7 +338,7 @@ t.only().describe('watcher', function() { var home = builder.getPath('home') 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() }) watcher.on('ready', function() { @@ -372,7 +353,7 @@ t.only().describe('watcher', function() { builder.newRandomFiles(dir, 100).then(names => { 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() }) @@ -386,7 +367,7 @@ t.only().describe('watcher', function() { var file = 'home/b/file1' var fpath = builder.getPath(file) 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() }) watcher.on('ready', function() { @@ -400,7 +381,7 @@ t.only().describe('watcher', function() { var file = 'home/a/newfile_' + builder.randomName() 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() }) watcher.on('ready', function() { @@ -414,7 +395,7 @@ t.only().describe('watcher', function() { t.test('should watch recursively with `recursive: true` option', function(done) { var dir = builder.getPath('home') 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) { done() } @@ -442,7 +423,7 @@ t.only().describe('watcher', function() { t.test('should throw on invalid encoding', function(done) { options.encoding = 'unknown' try { - watcher = watch(fdir, options) + watcher = new Watcher(fdir, options) } catch (e) { done() } @@ -450,7 +431,7 @@ t.only().describe('watcher', function() { t.test('should accept an encoding string', function(done) { 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) })) watcher.on('ready', function() { @@ -460,7 +441,7 @@ t.only().describe('watcher', function() { t.test('should support buffer encoding', function(done) { 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.strictEqual(name.toString(), fpath) })) @@ -472,7 +453,7 @@ t.only().describe('watcher', function() { t.test('should support base64 encoding', function(done) { options.encoding = 'base64' - watcher = watch(fdir, options, done.finish(function(evt, name) { + watcher = new Watcher(fdir, options, done.finish(function(evt, name) { assert.strictEqual( name, Buffer.from(fpath).toString('base64'), @@ -486,7 +467,7 @@ t.only().describe('watcher', function() { t.test('should support hex encoding', function(done) { options.encoding = 'hex' - watcher = watch(fdir, options, done.finish(function(evt, name) { + watcher = new Watcher(fdir, options, done.finish(function(evt, name) { assert.strictEqual( name, 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)) { matchIgnoredDir = true } else { @@ -552,7 +533,7 @@ t.only().describe('watcher', function() { var times = 0 var matchIgnoredFile = false - watcher = watch(dir, options, function(evt, name) { + watcher = new Watcher(dir, options, function(evt, name) { if (name === builder.getPath(file1)) { matchIgnoredFile = true } @@ -585,7 +566,7 @@ t.only().describe('watcher', function() { var times = 0 var matchIgnoredFile = false - watcher = watch(dir, options, function(evt, name) { + watcher = new Watcher(dir, options, function(evt, name) { if (name === builder.getPath(file1)) { matchIgnoredFile = true } @@ -605,31 +586,21 @@ t.only().describe('watcher', function() { var options = { delay: 0, recursive: true, + manualRecursive: true, filter: function(name, skip) { if (/\/deep_node_modules/.test(name)) return skip } } - watcher = watch(home, options) + watcher = new Watcher(home, options) - watcher.getWatchedPaths(function(paths) { - hasNativeRecursive(function(supportRecursive) { - 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) - }) + watcher.on('ready', done.finish(function() { + let homeFiltered = builder.getAllDirectories().filter(function(name) { + return !/\/deep_node_modules/.test(name) + }).sort() + let watchersPaths = watcher.listeners.map(x => x.path).sort() - assert.deepStrictEqual( - watched.sort(), paths.sort() - ) - - done() - }) - }) + assert.deepStrictEqual(watchersPaths, homeFiltered) + })) }) }) }) @@ -637,7 +608,7 @@ t.only().describe('watcher', function() { t.describe('parameters', function() { t.test('should throw error on non-existed file', function(done) { var somedir = builder.getPath('home/somedir') - watcher = watch(somedir) + watcher = new Watcher(somedir) watcher.on('error', done.finish(function(err) { assert.match(err.message, /not exist/) })) @@ -645,7 +616,7 @@ t.only().describe('watcher', function() { t.test('should accept filename as Buffer', function(done) { 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) })) watcher.on('ready', function() { @@ -667,7 +638,7 @@ t.only().describe('watcher', function() { builder.newFile(file1), builder.newFile(file2), ]).then(function() { - watcher = watch(fpaths, { delay: 0 }, function(evt, name) { + watcher = new Watcher(fpaths, { delay: 0 }, function(evt, name) { if (set.has(name)) { set.delete(name) counter.count() @@ -696,9 +667,8 @@ t.only().describe('watcher', function() { builder.getPath(file2) ] - console.log() 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) counter.count() }) @@ -728,7 +698,7 @@ t.only().describe('watcher', function() { var file = 'home/a/file1' var fpath = builder.getPath(file) - watcher = watch(dir, { delay: 0 }) + watcher = new Watcher(dir, { delay: 0 }) watcher.on('change', done.finish(function(evt, name) { assert.strictEqual(evt, 'update') @@ -736,7 +706,7 @@ t.only().describe('watcher', 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 file = 'home/a/file1' var times = 0 - watcher = watch(dir, { delay: 0 }) + watcher = new Watcher(dir, { delay: 0 }) watcher.on('change', function(evt, name) { times++ }) @@ -766,76 +736,16 @@ t.only().describe('watcher', function() { t.test('should not watch after .close() is called', function(done) { var dir = builder.getPath('home') - watcher = watch(dir, { delay: 0, recursive: true }) - watcher.close() + watcher = new Watcher(dir, { delay: 0, recursive: true }) - watcher.getWatchedPaths(done.finish(function(dirs) { - assert.strictEqual(dirs.length, 0) + watcher.on('ready', function() { + watcher.close() + }) + + watcher.on('close', done.finish(function(dirs) { + 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() - ) - })) - }) - }) - }) }) }) \ No newline at end of file diff --git a/test/watch/builder.mjs b/test/watch/builder.mjs index 7025f71..066c805 100644 --- a/test/watch/builder.mjs +++ b/test/watch/builder.mjs @@ -135,6 +135,8 @@ Builder.prototype.modify = function(fpath, delay) { Builder.prototype.remove = function(fpath) { let filePath = this.getPath(fpath) 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) {