diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dc578c..70548b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,12 @@ name: CI on: - - push - - pull_request + push: + branches: [master, main] + pull_request: + branches: [master, main] jobs: - test: runs-on: ${{ matrix.os }} @@ -13,13 +14,14 @@ jobs: fail-fast: false matrix: os: [macos-latest, ubuntu-latest] - node-version: [14.x, 18.x, 20.x] + node-version: [18.x, 20.x, 24.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - run: npm install + cache: 'npm' + - run: npm ci - run: npm test diff --git a/.gitignore b/.gitignore index 4bf3a39..727441d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ node_modules test/utils/__TREE__ .nyc_output/ coverage/ -package-lock.json .DS_Store diff --git a/Changelog.md b/Changelog.md index 8c5da97..6304943 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,11 +1,26 @@ # Changelog +## 0.8.0 + +### Breaking Changes +* Minimum Node.js version is now 18.0.0 (previously 6.0.0). + +### Improvements +* Modernized codebase - Converted all code to ES6+. +* Removed `fs-extra` - replaced with native Node.js `fs` module functions. +* Improved TypeScript definitions. +* Added `exports` field** in package.json for better ESM compatibility. + +

+ + ## 0.7.4 * Fix: add export to interface #128 (by @multivoltage) * Catch fs.watch exceptions #125 (by @campersau ) * Fix can't listener error event on incorrect file/directory #123 (by @leijuns) +

diff --git a/README.md b/README.md index c0f8bd2..a7576ec 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,16 @@ A wrapper and enhancements for [fs.watch](http://nodejs.org/api/fs.html#fs_fs_wa npm install node-watch ``` +## Requirements + +- Node.js >= 18.0.0 + ## Example ```js -var watch = require('node-watch'); +const watch = require('node-watch'); -watch('file_or_dir', { recursive: true }, function(evt, name) { +watch('file_or_dir', { recursive: true }, (evt, name) => { console.log('%s changed.', name); }); ``` @@ -91,7 +95,7 @@ The usage and options of `node-watch` are compatible with [fs.watch](https://nod ```js const pm = require('picomatch'); - let isMatch = pm('*.js'); + const isMatch = pm('*.js'); watch('./', { filter: f => isMatch(f) @@ -112,13 +116,13 @@ The usage and options of `node-watch` are compatible with [fs.watch](https://nod The events provided by the callback function is either `update` or `remove`, which is less confusing to `fs.watch`'s `rename` or `change`. ```js -watch('./', function(evt, name) { +watch('./', (evt, name) => { - if (evt == 'update') { + if (evt === 'update') { // on create or modify } - if (evt == 'remove') { + if (evt === 'remove') { // on delete } @@ -133,17 +137,17 @@ The watch function returns a [fs.FSWatcher](https://nodejs.org/api/fs.html#fs_cl #### Watcher events ```js -let watcher = watch('./', { recursive: true }); +const watcher = watch('./', { recursive: true }); -watcher.on('change', function(evt, name) { +watcher.on('change', (evt, name) => { // callback }); -watcher.on('error', function(err) { +watcher.on('error', (err) => { // handle error }); -watcher.on('ready', function() { +watcher.on('ready', () => { // the watcher is ready to respond to changes }); ``` @@ -198,7 +202,7 @@ watch(['file1', 'file2'], console.log); // https://github.com/nodejs/node-v0.x-archive/issues/3211 require('epipebomb')(); -let watcher = require('node-watch')( +const watcher = require('node-watch')( process.argv[2] || './', { recursive: true }, console.log ); @@ -217,6 +221,19 @@ Follow this description to increase the limit: [https://confluence.jetbrains.com/display/IDEADEV/Inotify+Watches+Limit](https://confluence.jetbrains.com/display/IDEADEV/Inotify+Watches+Limit) +## TypeScript + +This package includes TypeScript type definitions: + +```ts +import watch, { Watcher, EventType } from 'node-watch'; + +const watcher: Watcher = watch('./src', { recursive: true }, (evt: EventType, name: string) => { + console.log('%s changed.', name); +}); +``` + + ## Alternatives * [chokidar](https://github.com/paulmillr/chokidar) @@ -230,4 +247,4 @@ Thanks goes to [all wonderful people](https://github.com/yuanchuan/node-watch/gr ## License MIT -Copyright (c) 2012-2021 [yuanchuan](https://github.com/yuanchuan) +Copyright (c) 2012-2026 [yuanchuan](https://github.com/yuanchuan) diff --git a/lib/has-native-recursive.js b/lib/has-native-recursive.js index 49092be..252d792 100644 --- a/lib/has-native-recursive.js +++ b/lib/has-native-recursive.js @@ -1,80 +1,101 @@ -var fs = require('fs'); -var os = require('os'); -var path = require('path'); -var is = require('./is'); - -var IS_SUPPORT; -var TEMP_DIR = os.tmpdir && os.tmpdir() - || process.env.TMPDIR - || process.env.TEMP - || process.cwd(); - -function TempStack() { - this.stack = []; -} +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const is = require('./is'); + +let IS_SUPPORT; +const TEMP_DIR = os.tmpdir?.() || process.env.TMPDIR || process.env.TEMP || process.cwd(); + +/** + * Manages temporary files/directories for testing native recursive support + */ +class TempStack { + constructor() { + this.stack = []; + } -TempStack.prototype = { - create: function(type, base) { - var name = path.join(base, - 'node-watch-' + Math.random().toString(16).substr(2) - ); - this.stack.push({ name: name, type: type }); + /** + * Create a unique temporary path + * @param {'file'|'dir'} type - Type of path to create + * @param {string} base - Base directory + * @returns {string} - The created path name + */ + create(type, base) { + const name = path.join(base, `node-watch-${Math.random().toString(16).slice(2)}`); + this.stack.push({ name, type }); return name; - }, - write: function(/* file */) { - for (var i = 0; i < arguments.length; ++i) { - fs.writeFileSync(arguments[i], ' '); + } + + /** + * Write content to files + * @param {...string} files - File paths to write to + */ + write(...files) { + for (const file of files) { + fs.writeFileSync(file, ' '); } - }, - mkdir: function(/* dirs */) { - for (var i = 0; i < arguments.length; ++i) { - fs.mkdirSync(arguments[i]); + } + + /** + * Create directories + * @param {...string} dirs - Directory paths to create + */ + mkdir(...dirs) { + for (const dir of dirs) { + fs.mkdirSync(dir); } - }, - cleanup: function(fn) { + } + + /** + * Clean up all created files and directories + * @param {Function} [fn] - Optional callback after cleanup + */ + cleanup(fn) { try { - var temp; + let temp; while ((temp = this.stack.pop())) { - var type = temp.type; - var name = temp.name; + const { type, name } = temp; if (type === 'file' && is.file(name)) { fs.unlinkSync(name); - } - else if (type === 'dir' && is.directory(name)) { + } else if (type === 'dir' && is.directory(name)) { fs.rmdirSync(name); } } - } - finally { + } finally { if (is.func(fn)) fn(); } } -}; +} -var pending = false; +let pending = false; -module.exports = function hasNativeRecursive(fn) { +/** + * Detect if the platform supports native recursive watching + * @param {Function} fn - Callback with boolean result + * @returns {boolean|undefined} + */ +function hasNativeRecursive(fn) { if (!is.func(fn)) { return false; } + if (IS_SUPPORT !== undefined) { return fn(IS_SUPPORT); } if (!pending) { pending = true; - } - // check again later - else { - return setTimeout(function() { - hasNativeRecursive(fn); - }, 300); + } else { + // Check again later if detection is already in progress + return setTimeout(() => hasNativeRecursive(fn), 300); } - var stack = new TempStack(); - var parent = stack.create('dir', TEMP_DIR); - var child = stack.create('dir', parent); - var file = stack.create('file', child); + const stack = new TempStack(); + const parent = stack.create('dir', TEMP_DIR); + const child = stack.create('dir', parent); + const file = stack.create('file', child); try { stack.mkdir(parent, child); @@ -88,38 +109,43 @@ module.exports = function hasNativeRecursive(fn) { stack.mkdir(parent, child); } - var options = { recursive: true }; - var watcher; + const options = { recursive: true }; + let watcher; try { watcher = fs.watch(parent, options); } catch (e) { - if (e.code == 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') { - return fn(IS_SUPPORT = false); - } else { - throw e; + if (e.code === 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') { + IS_SUPPORT = false; + return fn(IS_SUPPORT); } + throw e; } if (!watcher) { return false; } - var timer = setTimeout(function() { + const timer = setTimeout(() => { watcher.close(); - stack.cleanup(function() { - fn(IS_SUPPORT = false); + stack.cleanup(() => { + IS_SUPPORT = false; + fn(IS_SUPPORT); }); }, 200); - watcher.on('change', function(evt, name) { + watcher.on('change', (evt, name) => { if (path.basename(file) === path.basename(name)) { watcher.close(); clearTimeout(timer); - stack.cleanup(function() { - fn(IS_SUPPORT = true); + stack.cleanup(() => { + IS_SUPPORT = true; + fn(IS_SUPPORT); }); } }); + stack.write(file); } + +module.exports = hasNativeRecursive; diff --git a/lib/is.js b/lib/is.js index ebe0600..1d3bcc7 100644 --- a/lib/is.js +++ b/lib/is.js @@ -1,12 +1,25 @@ -var fs = require('fs'); -var path = require('path'); -var os = require('os'); +'use strict'; -function matchObject(item, str) { - return Object.prototype.toString.call(item) - === '[object ' + str + ']'; +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +/** + * Check if an object matches a specific type string + * @param {*} item - The item to check + * @param {string} type - The type name to match + * @returns {boolean} + */ +function matchObject(item, type) { + return Object.prototype.toString.call(item) === `[object ${type}]`; } +/** + * Safely check file stats, handling permission errors gracefully + * @param {string} name - File path to check + * @param {Function} fn - Stat check function + * @returns {boolean} + */ function checkStat(name, fn) { try { return fn(name); @@ -21,56 +34,133 @@ function checkStat(name, fn) { } } -var is = { - nil: function(item) { +const is = { + /** + * Check if value is null or undefined + * @param {*} item + * @returns {boolean} + */ + nil(item) { return item == null; }, - array: function(item) { + + /** + * Check if value is an array + * @param {*} item + * @returns {boolean} + */ + array(item) { return Array.isArray(item); }, - emptyObject: function(item) { - for (var key in item) { + + /** + * Check if object is empty (has no own enumerable properties) + * @param {Object} item + * @returns {boolean} + */ + emptyObject(item) { + for (const key in item) { return false; } return true; }, - buffer: function(item) { + + /** + * Check if value is a Buffer + * @param {*} item + * @returns {boolean} + */ + buffer(item) { return Buffer.isBuffer(item); }, - regExp: function(item) { + + /** + * Check if value is a RegExp + * @param {*} item + * @returns {boolean} + */ + regExp(item) { return matchObject(item, 'RegExp'); }, - string: function(item) { + + /** + * Check if value is a string + * @param {*} item + * @returns {boolean} + */ + string(item) { return matchObject(item, 'String'); }, - func: function(item) { + + /** + * Check if value is a function + * @param {*} item + * @returns {boolean} + */ + func(item) { return typeof item === 'function'; }, - number: function(item) { + + /** + * Check if value is a number + * @param {*} item + * @returns {boolean} + */ + number(item) { return matchObject(item, 'Number'); }, - exists: function(name) { + + /** + * Check if a file or directory exists + * @param {string} name - Path to check + * @returns {boolean} + */ + exists(name) { return fs.existsSync(name); }, - file: function(name) { - return checkStat(name, function(n) { - return fs.statSync(n).isFile() - }); + + /** + * Check if path is a file + * @param {string} name - Path to check + * @returns {boolean} + */ + file(name) { + return checkStat(name, (n) => fs.statSync(n).isFile()); }, - samePath: function(a, b) { + + /** + * Check if two paths resolve to the same location + * @param {string} a - First path + * @param {string} b - Second path + * @returns {boolean} + */ + samePath(a, b) { return path.resolve(a) === path.resolve(b); }, - directory: function(name) { - return checkStat(name, function(n) { - return fs.statSync(n).isDirectory() - }); + + /** + * Check if path is a directory + * @param {string} name - Path to check + * @returns {boolean} + */ + directory(name) { + return checkStat(name, (n) => fs.statSync(n).isDirectory()); }, - symbolicLink: function(name) { - return checkStat(name, function(n) { - return fs.lstatSync(n).isSymbolicLink(); - }); + + /** + * Check if path is a symbolic link + * @param {string} name - Path to check + * @returns {boolean} + */ + symbolicLink(name) { + return checkStat(name, (n) => fs.lstatSync(n).isSymbolicLink()); }, - windows: function() { + + /** + * Check if running on Windows + * @returns {boolean} + */ + windows() { return os.platform() === 'win32'; } }; diff --git a/lib/watch.d.ts b/lib/watch.d.ts index f59f17d..ef7a22b 100644 --- a/lib/watch.d.ts +++ b/lib/watch.d.ts @@ -1,75 +1,179 @@ -import { FSWatcher } from 'fs'; +import {FSWatcher} from 'fs'; +import {EventEmitter} from 'events'; /** - * Watch for changes on `filename`, where filename is either a file or a directory. - * The second argument is optional. + * Watch for changes on files or directories. * - * If `options` is provided as a string, it specifies the encoding. - * Otherwise `options` should be passed as an object. + * @param pathName - File or directory to watch. Can be a single path or array of paths. + * @param options - Watch options or encoding string. + * @param callback - Callback function invoked on changes. + * @returns A Watcher object that can be used to manage the watch. * - * The listener callback gets two arguments, `(eventType, filePath)`, - * which is the same with `fs.watch`. - * `eventType` is either `update` or `remove`, - * `filePath` is the name of the file which triggered the event. + * @example + * ```js + * import watch from 'node-watch'; * - * @param {Filename} filename File or directory to watch. - * @param {Options|string} options - * @param {Function} callback + * // Watch a directory recursively + * const watcher = watch('./src', { recursive: true }, (evt, name) => { + * console.log('%s changed.', name); + * }); + * + * // Close the watcher when done + * watcher.close(); + * ``` */ declare function watch(pathName: PathName): Watcher; -declare function watch(pathName: PathName, options: Options) : Watcher; -declare function watch(pathName: PathName, callback: Callback): Watcher; -declare function watch(pathName: PathName, options: Options, callback: Callback): Watcher; +declare function watch(pathName: PathName,options: Options): Watcher; +declare function watch(pathName: PathName,callback: Callback): Watcher; +declare function watch(pathName: PathName,options: Options,callback: Callback): Watcher; + +/** + * Event type emitted by the watcher. + * - `update`: File or directory was created or modified. + * - `remove`: File or directory was deleted. + */ +export type EventType='update'|'remove'; + +/** + * Callback function invoked when a file system event occurs. + * @param eventType - The type of event (update or remove). + * @param filePath - The path of the file or directory that changed. + */ +export type Callback=(eventType: EventType,filePath: string) => void; + +/** + * Path name to watch. Can be a single path or an array of paths. + */ +export type PathName=string|readonly string[]; + +/** + * Return value from a filter function. + * - `true`: Include the file/directory. + * - `false`: Exclude the file/directory. + * - `skip` symbol: Exclude and don't recurse into subdirectories. + */ +export type FilterReturn=boolean|symbol; -type EventType = 'update' | 'remove'; -type Callback = (eventType: EventType, filePath: string) => any; -type PathName = string | Array; -type FilterReturn = boolean | symbol; +/** + * Filter function for selectively watching files and directories. + * @param file - The file or directory path being considered. + * @param skip - A symbol that can be returned to skip a directory and its subdirectories. + * @returns Whether to include the file/directory, or the skip symbol. + */ +export type FilterFunction=(file: string,skip: symbol) => FilterReturn; -type Options = { +/** + * Options for configuring the watcher. + */ +export interface Options { /** * Indicates whether the process should continue to run * as long as files are being watched. * @default true */ - persistent ?: boolean; + persistent?: boolean; /** * Indicates whether all subdirectories should be watched. * @default false */ - recursive ?: boolean; + recursive?: boolean; /** * Specifies the character encoding to be used for the filename - * passed to the listener. + * passed to the listener. Use 'buffer' to receive Buffer objects. * @default 'utf8' */ - encoding ?: string; + encoding?: BufferEncoding|'buffer'; /** - * Only files which pass this filter (when it returns `true`) - * will be sent to the listener. + * Only files which pass this filter will trigger events. + * Can be a RegExp or a function. + * + * @example + * ```js + * // Filter with RegExp + * watch('./', { filter: /\.js$/ }); + * + * // Filter with function + * watch('./', { + * filter: (f, skip) => { + * if (/node_modules/.test(f)) return skip; + * return /\.js$/.test(f); + * } + * }); + * ``` */ - filter ?: RegExp | ((file: string, skip: symbol) => FilterReturn); + filter?: RegExp|FilterFunction; /** - * Delay time of the callback function. + * Delay in milliseconds before triggering the callback. + * Events that occur within this window are deduplicated. * @default 200 */ - delay ?: number; -}; + delay?: number; +} -export declare interface Watcher extends FSWatcher { +/** + * Extended FSWatcher interface with additional methods. + */ +export interface Watcher extends Pick { /** * Returns `true` if the watcher has been closed. */ isClosed(): boolean; /** - * Returns all watched paths. + * Close the watcher and stop watching for changes. + */ + close(): void; + + /** + * Get all watched paths asynchronously. + * @param callback - Function called with array of watched paths. + */ + getWatchedPaths(callback: (paths: string[]) => void): void; + + /** + * Listen for change events. + */ + on(event: 'change',listener: Callback): this; + + /** + * Listen for error events. + */ + on(event: 'error',listener: (error: Error) => void): this; + + /** + * Listen for when the watcher is ready. + */ + on(event: 'ready',listener: () => void): this; + + /** + * Listen for when the watcher is closed. + */ + on(event: 'close',listener: () => void): this; + + /** + * Listen once for change events. + */ + once(event: 'change',listener: Callback): this; + + /** + * Listen once for error events. + */ + once(event: 'error',listener: (error: Error) => void): this; + + /** + * Listen once for when the watcher is ready. + */ + once(event: 'ready',listener: () => void): this; + + /** + * Listen once for when the watcher is closed. */ - getWatchedPaths(): Array; + once(event: 'close',listener: () => void): this; } +export {watch}; export default watch; diff --git a/lib/watch.js b/lib/watch.js index 73f1c38..1be7733 100644 --- a/lib/watch.js +++ b/lib/watch.js @@ -1,155 +1,184 @@ -var fs = require('fs'); -var path = require('path'); -var util = require('util'); -var events = require('events'); +'use strict'; -var hasNativeRecursive = require('./has-native-recursive'); -var is = require('./is'); +const fs = require('fs'); +const path = require('path'); +const { EventEmitter } = require('events'); -var EVENT_UPDATE = 'update'; -var EVENT_REMOVE = 'remove'; +const hasNativeRecursive = require('./has-native-recursive'); +const is = require('./is'); -var SKIP_FLAG = Symbol('skip'); +// Event type constants +const EVENT_UPDATE = 'update'; +const EVENT_REMOVE = 'remove'; +// Symbol for skip flag in filter function +const SKIP_FLAG = Symbol('skip'); + +/** + * Check if array contains duplicates + * @param {Array} arr + * @returns {boolean} + */ function hasDup(arr) { - return arr.some(function(v, i, self) { - return self.indexOf(v) !== i; - }); + return arr.some((v, i, self) => self.indexOf(v) !== i); } +/** + * Remove duplicates from array + * @param {Array} arr + * @returns {Array} + */ function unique(arr) { - return arr.filter(function(v, i, self) { - return self.indexOf(v) === i; - }); -} - -// One level flat -function flat1(arr) { - return arr.reduce(function(acc, v) { - return acc.concat(v); - }, []); + return arr.filter((v, i, self) => self.indexOf(v) === i); } +/** + * Validate encoding option + * @param {string} encoding + * @throws {Error} If encoding is unknown + */ function assertEncoding(encoding) { if (encoding && encoding !== 'buffer' && !Buffer.isEncoding(encoding)) { - throw new Error('Unknown encoding: ' + encoding); + throw new Error(`Unknown encoding: ${encoding}`); } } +/** + * Create a filter guard function from filter option + * @param {Function|RegExp} fn - Filter function or RegExp + * @returns {Function} Guard function that calls action if filter passes + */ function guard(fn) { if (is.func(fn)) { - return function(arg, action) { + return (arg, action) => { if (fn(arg, false)) action(); - } + }; } if (is.regExp(fn)) { - return function(arg, action) { + return (arg, action) => { if (fn.test(arg)) action(); - } - } - return function(arg, action) { - action(); + }; } + return (arg, action) => action(); } +/** + * Convert file names to event messages + * @param {string[]} names - File names + * @returns {Array<[string, string]>} Array of [eventType, fileName] tuples + */ function composeMessage(names) { - return names.map(function(n) { - return is.exists(n) - ? [EVENT_UPDATE, n] - : [EVENT_REMOVE, n]; - }); + return names.map((n) => (is.exists(n) ? [EVENT_UPDATE, n] : [EVENT_REMOVE, n])); } +/** + * Process cached file names and generate event messages + * Filters out temporary editor files when saving + * @param {string[]} cache - Cached file names + * @returns {Array<[string, string]>} Array of [eventType, fileName] tuples + */ function getMessages(cache) { - var filtered = unique(cache); + let 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); - }); + const reg = /~$|^\.#|^##$/g; + const hasSpecialChar = cache.some((c) => reg.test(c)); if (hasSpecialChar) { - var dup = hasDup(cache.map(function(c) { - return c.replace(reg, ''); - })); + const dup = hasDup(cache.map((c) => c.replace(reg, ''))); if (dup) { - filtered = filtered.filter(function(m) { - return is.exists(m); - }); + filtered = filtered.filter((m) => is.exists(m)); } } return composeMessage(filtered); } +/** + * Create a debounced event handler + * @param {Object} info - Watch info object + * @param {Function} fn - Callback function + * @returns {Function} Debounced handler + */ function debounce(info, fn) { - var timer, cache = []; - var encoding = info.options.encoding; - var delay = info.options.delay; - if (!is.number(delay)) { - delay = 200; - } + let timer = null; + let cache = []; + const { encoding } = info.options; + const delay = is.number(info.options.delay) ? info.options.delay : 200; + function handle() { - getMessages(cache).forEach(function(msg) { + getMessages(cache).forEach((msg) => { msg[1] = Buffer.from(msg[1]); if (encoding !== 'buffer') { msg[1] = msg[1].toString(encoding); } - fn.apply(null, msg); + fn(...msg); }); timer = null; cache = []; } - return function(rawEvt, name) { + + return (rawEvt, name) => { cache.push(name); if (!timer) { timer = setTimeout(handle, delay); } - } + }; } +/** + * Create a duplicate event filter for composed watchers + * @returns {Function} Filter function + */ 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 = {}; + let memo = {}; + return (fn) => (evt, name) => { + memo[evt + name] = [evt, name]; + setTimeout(() => { + Object.keys(memo).forEach((n) => { + fn(...memo[n]); }); - } - } + memo = {}; + }); + }; } +/** + * Safely start watching a path + * @param {Watcher} watcher - Watcher instance + * @param {string} dir - Directory to watch + * @param {Object} opts - Watch options + * @returns {fs.FSWatcher|undefined} + */ function tryWatch(watcher, dir, opts) { try { return fs.watch(dir, opts); } catch (e) { - process.nextTick(function() { - watcher.emit('error', e); - }); + process.nextTick(() => watcher.emit('error', e)); } } -function getSubDirectories(dir, fn, done = function() {}) { +/** + * Get subdirectories of a directory + * @param {string} dir - Directory path + * @param {Function} fn - Callback for each subdirectory + * @param {Function} [done] - Callback when complete + */ +function getSubDirectories(dir, fn, done = () => {}) { if (is.directory(dir)) { - fs.readdir(dir, function(err, all) { + fs.readdir(dir, (err, all) => { if (err) { - // don't throw permission errors. + // Don't throw permission errors if (/^(EPERM|EACCES)$/.test(err.code)) { console.warn('Warning: Cannot access %s.', dir); } else { throw err; } - } - else { - all.forEach(function(f) { - var sdir = path.join(dir, f); + } else { + all.forEach((f) => { + const sdir = path.join(dir, f); if (is.directory(sdir)) fn(sdir); }); done(); @@ -160,8 +189,13 @@ function getSubDirectories(dir, fn, done = function() {}) { } } +/** + * Create a semaphore for tracking async operations + * @param {Function} final - Callback when all operations complete + * @returns {Function} Start function + */ function semaphore(final) { - var counter = 0; + let counter = 0; return function start() { counter++; return function stop() { @@ -171,375 +205,419 @@ function semaphore(final) { }; } +/** + * Null counter for single directory watching + * @returns {Function} + */ function nullCounter() { - return function nullStop() {}; + return () => {}; } +/** + * Check if a path should be watched based on filter + * @param {string} filePath - Path to check + * @param {Function} filter - Filter function + * @returns {boolean} + */ 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); - } - }); - 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)); - }); - } - } -} - +/** + * Emit ready event on watcher + * @param {Watcher} self - Watcher instance + */ 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'); - }); + process.nextTick(() => self.emit('ready')); } } +/** + * Emit close event on watcher + * @param {Watcher} self - Watcher instance + */ 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; +/** + * Watcher class extending EventEmitter + * Manages file system watchers for files and directories + */ +class Watcher extends EventEmitter { + constructor() { + super(); + this.watchers = {}; + this._isReady = false; + this._isClosed = false; + this.flag = ''; + } - // Internal callback for handling fs.FSWatcher 'change' events - var internalOnChange = function(rawEvt, rawName) { - if (self.isClosed()) { - return; - } + /** + * Create an exposed interface for external use + * @returns {Object} Exposed methods + */ + expose() { + const methods = [ + 'on', + 'emit', + 'once', + 'close', + 'isClosed', + 'listeners', + 'setMaxListeners', + 'getMaxListeners', + 'getWatchedPaths' + ]; + + const expose = {}; + methods.forEach((name) => { + expose[name] = (...args) => this[name](...args); + }); + return expose; + } - // 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); - } - } + /** + * Check if watcher is closed + * @returns {boolean} + */ + isClosed() { + return this._isClosed; + } + + /** + * Close watcher for a specific path or all paths + * @param {string} [fullPath] - Specific path to close, or undefined for all + */ + close(fullPath) { + if (fullPath) { + const watcher = this.watchers[fullPath]; + if (watcher?.close) { + watcher.close(); + delete this.watchers[fullPath]; + } + getSubDirectories(fullPath, (fpath) => this.close(fpath)); + } else { + Object.keys(this.watchers).forEach((fpath) => { + const watcher = this.watchers[fpath]; + if (watcher?.close) { + watcher.close(); } }); + this.watchers = {}; } - 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); + // Do not close the Watcher unless all child watchers are closed + // https://github.com/yuanchuan/node-watch/issues/75 + if (is.emptyObject(this.watchers)) { + if (!this._isClosed) { + this._isClosed = true; + process.nextTick(emitClose, this); } } - // 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') { - watcher.emit('change', EVENT_REMOVE, info.fpath && ''); - self.flag = 'windows-error'; - self.close(watcherPath); - } else { - self.emit('error', err); + /** + * Get all watched paths + * @param {Function} fn - Callback with array of paths + */ + getWatchedPaths(fn) { + if (is.func(fn)) { + if (this._isReady) { + fn(Object.keys(this.watchers)); + } else { + this.on('ready', () => fn(Object.keys(this.watchers))); + } } - }); + } - watcher.on('change', internalOnChange); -} + /** + * Add a native FSWatcher to this watcher + * @param {fs.FSWatcher} watcher - Native watcher + * @param {Object} info - Watch info + */ + add(watcher, info = { fpath: '' }) { + const watcherPath = path.resolve(info.fpath); + this.watchers[watcherPath] = watcher; + + // Debounced handler for public events + const handlePublicEvents = debounce(info, (evt, name) => { + // Watch single file + if (info.compareName) { + if (info.compareName(name)) { + this.emit('change', evt, name); + } + } else { + // Watch directory + const filterGuard = guard(info.options.filter); + filterGuard(name, () => { + if (this.flag) { + this.flag = ''; + } else { + this.emit('change', evt, name); + } + }); + } + }); -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' - }); + // Internal callback for handling fs.FSWatcher 'change' events + const internalOnChange = (rawEvt, rawName) => { + if (this.isClosed()) { + return; + } - // no need to watch recursively - delete opts.recursive; + // Normalize lack of name and convert to full path + let name = rawName ?? ''; + name = path.join(info.fpath, name); + + if (info.options.recursive) { + hasNativeRecursive((has) => { + if (!has) { + const fullPath = path.resolve(name); + // Remove watcher on removal + if (!is.exists(name)) { + this.close(fullPath); + } else { + // Watch new created directory + const shouldWatch = + is.directory(name) && + !this.watchers[fullPath] && + shouldNotSkip(name, info.options.filter); + + if (shouldWatch) { + this.watchDirectory(name, info.options); + } + } + } + }); + } - var watcher = tryWatch(this, parent, opts); - if (!watcher) { - return; - } + handlePublicEvents(rawEvt, name); + }; - this.add(watcher, { - type: 'file', - fpath: parent, - options: Object.assign({}, opts, { - encoding: options.encoding - }), - compareName: function(n) { - return is.samePath(n, file); - } - }); + watcher.on('error', (err) => { + if (this.isClosed()) { + return; + } + if (is.windows() && err.code === 'EPERM') { + watcher.emit('change', EVENT_REMOVE, info.fpath ?? ''); + this.flag = 'windows-error'; + this.close(watcherPath); + } else { + this.emit('error', err); + } + }); - if (is.func(fn)) { - if (fn.length === 1) deprecationWarning(); - this.on('change', fn); + watcher.on('change', internalOnChange); } -} -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, { + /** + * Watch a single file + * @param {string} file - File path + * @param {Object} options - Watch options + * @param {Function} [fn] - Change callback + */ + watchFile(file, options, fn) { + const parent = path.join(file, '../'); + const opts = { + ...options, + filter: null, // No filter for single file encoding: 'utf8' - }); - if (!has) { - delete opts.recursive; - } + }; - // check if it's closed before calling watch. - if (self._isClosed) { - done(); - return self.close(); - } + // No need to watch recursively for single file + delete opts.recursive; - var watcher = tryWatch(self, dir, opts); + const watcher = tryWatch(this, parent, opts); if (!watcher) { - done(); return; } - self.add(watcher, { - type: 'dir', - fpath: dir, - options: options + this.add(watcher, { + type: 'file', + fpath: parent, + options: { ...opts, encoding: options.encoding }, + compareName: (n) => is.samePath(n, file) }); if (is.func(fn)) { - if (fn.length === 1) deprecationWarning(); - self.on('change', fn); + this.on('change', fn); } + } - if (options.recursive && !has) { - getSubDirectories(dir, function(d) { - if (shouldNotSkip(d, options.filter)) { - self.watchDirectory(d, options, null, counter); - } - }, counter()); - } + /** + * Watch a directory + * @param {string} dir - Directory path + * @param {Object} options - Watch options + * @param {Function} [fn] - Change callback + * @param {Function} [counter] - Semaphore counter + */ + watchDirectory(dir, options, fn, counter = nullCounter) { + const done = counter(); + + hasNativeRecursive((has) => { + // Always specify recursive + options.recursive = !!options.recursive; + + // Using utf8 internally + const opts = { ...options, encoding: 'utf8' }; + if (!has) { + delete opts.recursive; + } - done(); - }); + // Check if closed before calling watch + if (this._isClosed) { + done(); + return this.close(); + } + + const watcher = tryWatch(this, dir, opts); + if (!watcher) { + done(); + return; + } + + this.add(watcher, { + type: 'dir', + fpath: dir, + options + }); + + if (is.func(fn)) { + this.on('change', fn); + } + + if (options.recursive && !has) { + getSubDirectories( + dir, + (d) => { + if (shouldNotSkip(d, options.filter)) { + this.watchDirectory(d, options, null, counter); + } + }, + counter() + ); + } + + done(); + }); + } } +/** + * Compose multiple watchers into one + * @param {Watcher[]} watchers - Array of watchers + * @returns {Object} Composed watcher interface + */ 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)) { + const watcher = new Watcher(); + const filterDups = createDupsFilter(); + let counter = watchers.length; + + watchers.forEach((w) => { + w.on( + 'change', + filterDups((evt, name) => watcher.emit('change', evt, name)) + ); + w.on('error', (err) => watcher.emit('error', err)); + w.on('ready', () => { + if (!--counter) { emitReady(watcher); } }); }); - watcher.close = function() { - watchers.forEach(function(w) { - w.close(); - }); + watcher.close = () => { + watchers.forEach((w) => w.close()); process.nextTick(emitClose, watcher); - } + }; - watcher.getWatchedPaths = function(fn) { + watcher.getWatchedPaths = (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); + const promises = watchers.map( + (w) => new Promise((resolve) => w.getWatchedPaths(resolve)) + ); + Promise.all(promises).then((result) => { + fn(unique(result.flat(1))); }); } - } + }; return watcher.expose(); } +/** + * Watch files or directories for changes + * @param {string|Buffer|string[]} fpath - Path(s) to watch + * @param {Object|string|Function} [options] - Watch options or encoding string + * @param {Function} [fn] - Change callback + * @returns {Object} Watcher interface + */ function watch(fpath, options, fn) { - var watcher = new Watcher(); + const watcher = new Watcher(); + // Handle Buffer input if (is.buffer(fpath)) { fpath = fpath.toString(); } + // Validate path exists if (!is.array(fpath) && !is.exists(fpath)) { - process.nextTick(function() { - watcher.emit('error', - new Error(fpath + ' does not exist.') - ); + process.nextTick(() => { + watcher.emit('error', new Error(`${fpath} does not exist.`)); }); } + // Handle encoding string as options if (is.string(options)) { - options = { - encoding: options - } + options = { encoding: options }; } + // Handle callback as second argument if (is.func(options)) { fn = options; options = {}; } + // Default options if (arguments.length < 2) { options = {}; } + // Validate and set encoding if (options.encoding) { assertEncoding(options.encoding); } else { options.encoding = 'utf8'; } + // Handle array of paths 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; - })); + const filterDups = createDupsFilter(); + return composeWatcher( + unique(fpath).map((f) => { + const w = watch(f, options); + if (is.func(fn)) { + w.on('change', filterDups(fn)); + } + return w; + }) + ); } + // Watch file if (is.file(fpath)) { watcher.watchFile(fpath, options, fn); emitReady(watcher); } - + // Watch directory else if (is.directory(fpath)) { - var counter = semaphore(function () { - emitReady(watcher); - }); + const counter = semaphore(() => emitReady(watcher)); watcher.watchDirectory(fpath, options, fn, counter); } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8207e6b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1190 @@ +{ + "name": "node-watch", + "version": "0.8.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-watch", + "version": "0.8.0", + "license": "MIT", + "devDependencies": { + "mocha": "^11.7.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 48f9afb..28a678c 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,43 @@ { + "name": "node-watch", + "version": "0.8.0", "description": "A wrapper and enhancements for fs.watch", "license": "MIT", - "name": "node-watch", + "author": "yuanchuan (https://yuanchuan.dev)", "repository": { - "url": "git://github.com/yuanchuan/node-watch.git", - "type": "git" + "type": "git", + "url": "git://github.com/yuanchuan/node-watch.git" + }, + "homepage": "https://github.com/yuanchuan/node-watch#readme", + "bugs": { + "url": "https://github.com/yuanchuan/node-watch/issues" }, "keywords": [ "fs.watch", "watch", - "watchfile" + "watchfile", + "file watcher", + "directory watcher" ], - "version": "0.7.4", - "bugs": { - "url": "https://github.com/yuanchuan/node-watch/issues" - }, - "url": "https://github.com/yuanchuan/node-watch", - "author": "yuanchuan (http://yuanchuan.name)", - "main": "./lib/watch", + "main": "./lib/watch.js", "types": "./lib/watch.d.ts", + "exports": { + ".": { + "types": "./lib/watch.d.ts", + "require": "./lib/watch.js", + "default": "./lib/watch.js" + } + }, "files": [ "lib/" ], - "homepage": "https://github.com/yuanchuan/node-watch#readme", "scripts": { - "test": "mocha test/test.js --exit --slow 500" + "test": "mocha test/test.js --exit --slow 500 --timeout 5000 --retries 2" }, "engines": { - "node": ">=6" + "node": ">=18" }, "devDependencies": { - "fs-extra": "^7.0.1", - "mocha": "^10.2.0" + "mocha": "^11.7.5" } } diff --git a/test/test.js b/test/test.js index 3c1473d..8c58574 100644 --- a/test/test.js +++ b/test/test.js @@ -1,17 +1,19 @@ -var assert = require('assert'); -var Tree = require('./utils/builder'); -var watch = require('../lib/watch'); -var is = require('../lib/is'); -var hasNativeRecursive = require('../lib/has-native-recursive'); +'use strict'; -var tree = Tree(); -var watcher; +const assert = require('assert'); +const Tree = require('./utils/builder'); +const watch = require('../lib/watch'); +const is = require('../lib/is'); +const hasNativeRecursive = require('../lib/has-native-recursive'); -beforeEach(function() { +let tree = Tree(); +let watcher; + +beforeEach(() => { tree = Tree(); }); -afterEach(function(done) { +afterEach((done) => { if (watcher && !watcher.isClosed()) { watcher.on('close', done); watcher.close(); @@ -20,19 +22,24 @@ afterEach(function(done) { } }); -after(function() { +after(() => { if (tree) { tree.cleanup(); } }); -function wait(fn, timeout) { +/** + * Retry assertion until it passes or timeout + * @param {Function} fn - Assertion function + * @param {number} timeout - Timeout in milliseconds + */ +function wait(fn, timeout = 450) { try { fn(); } catch (error) { timeout -= 30; if (timeout >= 0) { - setTimeout(function() { + setTimeout(() => { wait(fn, timeout); }, 30); } else { @@ -41,115 +48,115 @@ function wait(fn, timeout) { } } -describe('process events', function() { - it('should emit `close` event', function(done) { - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(fpath, function() {}); - watcher.on('close', function() { +describe('process events', () => { + it('should emit `close` event', (done) => { + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(fpath, () => {}); + watcher.on('close', () => { done(); }); watcher.close(); }); - it('should emit `ready` event when watching a file', function(done) { - var file = 'home/a/file1'; - var fpath = tree.getPath(file); + it('should emit `ready` event when watching a file', (done) => { + const file = 'home/a/file1'; + const fpath = tree.getPath(file); watcher = watch(fpath); - watcher.on('ready', function() { + watcher.on('ready', () => { done(); }); }); - it('should emit `ready` event when watching a directory recursively', function(done) { - var dir = tree.getPath('home'); + it('should emit `ready` event when watching a directory recursively', (done) => { + const dir = tree.getPath('home'); watcher = watch(dir, { recursive: true }); - watcher.on('ready', function() { + watcher.on('ready', () => { done(); }); }); - it('should emit `ready` properly in a composed watcher', function(done) { - var dir1 = tree.getPath('home/a'); - var dir2 = tree.getPath('home/b'); - var file = tree.getPath('home/b/file1'); + it('should emit `ready` properly in a composed watcher', (done) => { + const dir1 = tree.getPath('home/a'); + const dir2 = tree.getPath('home/b'); + const file = tree.getPath('home/b/file1'); watcher = watch([dir1, dir2, file], { recursive: true }); - watcher.on('ready', function() { + watcher.on('ready', () => { done(); }); }); }); -describe('watch for files', function() { - it('should watch a single file and keep watching', function(done) { - var times = 1; - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(fpath, { delay: 0 }, function(evt, name) { - assert.equal(fpath, name) +describe('watch for files', () => { + it('should watch a single file and keep watching', (done) => { + let times = 1; + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(fpath, { delay: 0 }, (evt, name) => { + assert.strictEqual(fpath, name); if (times++ >= 3) { done(); } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file); tree.modify(file, 100); tree.modify(file, 200); }); }); - it('should watch files inside a directory', function(done) { - var fpath = tree.getPath('home/a'); - var stack = [ + it('should watch files inside a directory', (done) => { + const fpath = tree.getPath('home/a'); + const stack = [ tree.getPath('home/a/file1'), tree.getPath('home/a/file2') ]; - watcher = watch(fpath, { delay: 0 }, function(evt, name) { + watcher = watch(fpath, { delay: 0 }, (evt, name) => { stack.splice(stack.indexOf(name), 1); if (!stack.length) done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify('home/a/file1'); tree.modify('home/a/file2', 100); }); }); - it('should ignore duplicate changes', function(done) { - var file = 'home/a/file2'; - var fpath = tree.getPath(file); - var times = 0; - watcher = watch(fpath, { delay: 200 }, function(evt, name) { + it('should ignore duplicate changes', (done) => { + const file = 'home/a/file2'; + const fpath = tree.getPath(file); + let times = 0; + watcher = watch(fpath, { delay: 200 }, (evt, name) => { if (fpath === name) times++; }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file); tree.modify(file, 100); tree.modify(file, 150); - wait(function() { - assert.equal(times, 1) + wait(() => { + assert.strictEqual(times, 1); done(); - }, 250); + }); }); }); - it('should listen to new created files', function(done) { - var home = tree.getPath('home'); - var newfile1 = 'home/a/newfile' + Math.random(); - var newfile2 = 'home/a/newfile' + Math.random(); - var changes = []; - watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { + it('should listen to new created files', (done) => { + const home = tree.getPath('home'); + const newfile1 = 'home/a/newfile' + Math.random(); + const newfile2 = 'home/a/newfile' + Math.random(); + const changes = []; + watcher = watch(home, { delay: 0, recursive: true }, (evt, name) => { changes.push(name); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.newFile(newfile1); tree.newFile(newfile2); - wait(function() { + wait(() => { // On windows it will report its parent directory along with the filename // https://github.com/yuanchuan/node-watch/issues/79 if (is.windows()) { - // Make sure new files are deteced + // Make sure new files are detected assert.ok( changes.includes(tree.getPath(newfile1)) && changes.includes(tree.getPath(newfile2)) @@ -157,73 +164,70 @@ describe('watch for files', function() { // It should only include new files and its parent directory // if there are more than 2 events if (changes.length > 2) { - let accepts = [ + const accepts = [ tree.getPath(newfile1), tree.getPath(newfile2), tree.getPath('home/a') ]; - changes.forEach(function(name) { - assert.ok(accepts.includes(name), name + " should not be included"); + changes.forEach((name) => { + assert.ok(accepts.includes(name), name + ' should not be included'); }); } } else { assert.deepStrictEqual( - changes, - [tree.getPath(newfile1), tree.getPath(newfile2)] + changes.sort(), + [tree.getPath(newfile1), tree.getPath(newfile2)].sort() ); } done(); - }, 100); + }); }); }); - it('should error when parent gets deleted before calling fs.watch', function(done) { - var fpath = tree.getPath('home/a/file1'); + it('should error when parent gets deleted before calling fs.watch', (done) => { + const fpath = tree.getPath('home/a/file1'); watcher = watch(fpath, Object.defineProperty({}, 'test', { enumerable: true, - get: function() { + get() { tree.remove('home/a'); return 'test'; } })); - watcher.on('error', function() { + watcher.on('error', () => { done(); }); }); }); -describe('watch for directories', function() { - it('should watch directories inside a directory', function(done) { - var home = tree.getPath('home'); - var dir = tree.getPath('home/c'); - var events = []; +describe('watch for directories', () => { + it('should watch directories inside a directory', (done) => { + const home = tree.getPath('home'); + const dir = tree.getPath('home/c'); + const events = []; - watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { + watcher = watch(home, { delay: 0, recursive: true }, (evt, name) => { if (name === dir) { events.push(evt); } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.remove('home/c'); - wait(function () { - assert.deepStrictEqual( - events, - [ 'remove' ] - ); + wait(() => { + assert.deepStrictEqual(events, ['remove']); done(); - }, 400); + }); }); }); - it('should watch new created directories', function(done) { - var home = tree.getPath('home'); - watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { + it('should watch new created directories', (done) => { + const home = tree.getPath('home'); + watcher = watch(home, { delay: 0, recursive: true }, (evt, name) => { if (name === tree.getPath('home/new/file1')) { done(); } }); - watcher.on('ready', function() { + watcher.on('ready', () => { // newFile() will create the 'new/' directory and the 'new/file1' file, // but, only the creation of the directory is observed. // Because of that, there will only be one event for file1, when it @@ -233,169 +237,169 @@ describe('watch for directories', function() { }); }); - it('should not watch new created directories which are being skipped in the filter', function(done) { - var home = tree.getPath('home'); - var options = { + it('should not watch new created directories which are being skipped in the filter', (done) => { + const home = tree.getPath('home'); + const options = { delay: 0, recursive: true, - filter: function(filePath, skip) { + filter(filePath, skip) { if (/ignored/.test(filePath)) return skip; return true; } - } + }; - watcher = watch(home, options, function(evt, name) { - assert.fail("event detect", name); + watcher = watch(home, options, (evt, name) => { + assert.fail('event detect', name); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.newFile('home/ignored/file'); tree.modify('home/ignored/file', 100); - wait(done, 150); + wait(done); }); }); - it('should keep watching after removal of sub directory', function(done) { - var home = tree.getPath('home'); - var file1 = tree.getPath('home/e/file1'); - var file2 = tree.getPath('home/e/file2'); - var dir = tree.getPath('home/e/sub'); - var events = []; - watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { + it('should keep watching after removal of sub directory', (done) => { + const home = tree.getPath('home'); + const file1 = tree.getPath('home/e/file1'); + const file2 = tree.getPath('home/e/file2'); + const dir = tree.getPath('home/e/sub'); + const events = []; + watcher = watch(home, { delay: 0, recursive: true }, (evt, name) => { if (name === dir || name === file1 || name === file2) { events.push(name); } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.remove('home/e/sub', 50); tree.modify('home/e/file1', 100); tree.modify('home/e/file2', 200); - wait(function() { + wait(() => { assert.deepStrictEqual(events, [dir, file1, file2]); done(); - }, 300); + }); }); }); - it('should watch new directories without delay', function(done) { - var home = tree.getPath('home'); - var events = []; - watcher = watch(home, { delay: 200, recursive: true }, function(evt, name) { + it('should watch new directories without delay', (done) => { + const home = tree.getPath('home'); + const events = []; + watcher = watch(home, { delay: 200, recursive: true }, (evt, name) => { if (name === tree.getPath('home/new/file1')) { events.push(evt); } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.newFile('home/new/file1'); tree.modify('home/new/file1', 50); tree.modify('home/new/file1', 100); - wait(function() { + wait(() => { assert.deepStrictEqual(events, ['update']); done(); - }, 350); + }); }); }); - it('should error when directory gets deleted before calling fs.watch', function(done) { - var dir = 'home/c'; - var fpath = tree.getPath(dir); + it('should error when directory gets deleted before calling fs.watch', (done) => { + const dir = 'home/c'; + const fpath = tree.getPath(dir); watcher = watch(fpath, Object.defineProperty({}, 'test', { enumerable: true, - get: function() { + get() { tree.remove(dir); return 'test'; } })); - watcher.on('error', function() { + watcher.on('error', () => { done(); }); }); }); -describe('file events', function() { - it('should identify `remove` event', function(done) { - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(fpath, function(evt, name) { +describe('file events', () => { + it('should identify `remove` event', (done) => { + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(fpath, (evt, name) => { if (evt === 'remove' && name === fpath) done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.remove(file); }); }); - it('should identify `remove` event on directory', function(done) { - var dir = 'home/a'; - var home = tree.getPath('home'); - var fpath = tree.getPath(dir); - watcher = watch(home, function(evt, name) { + it('should identify `remove` event on directory', (done) => { + const dir = 'home/a'; + const home = tree.getPath('home'); + const fpath = tree.getPath(dir); + watcher = watch(home, (evt, name) => { if (evt === 'remove' && name === fpath) done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.remove(dir); }); }); - it('should be able to handle many events on deleting', function(done) { - var dir = 'home/a'; - var fpath = tree.getPath(dir); - var names = tree.newRandomFiles(dir, 300); + it('should be able to handle many events on deleting', (done) => { + const dir = 'home/a'; + const fpath = tree.getPath(dir); + const names = tree.newRandomFiles(dir, 300); - var count = 0; - watcher = watch(fpath, function(evt, name) { + let count = 0; + watcher = watch(fpath, () => { count += 1; - if (count == names.length) done(); + if (count === names.length) done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { names.forEach(tree.remove.bind(tree)); }); }); - it('should identify `update` event', function(done) { - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(fpath, function(evt, name) { + it('should identify `update` event', (done) => { + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(fpath, (evt, name) => { if (evt === 'update' && name === fpath) done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file); }); }); - it('should report `update` on new files', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/newfile' + Date.now(); - var fpath = tree.getPath(file); - watcher = watch(dir, function(evt, name) { + it('should report `update` on new files', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/newfile' + Date.now(); + const fpath = tree.getPath(file); + watcher = watch(dir, (evt, name) => { if (evt === 'update' && name === fpath) done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.newFile(file); }); }); }); -describe('options', function() { - describe('recursive', function() { - it('should watch recursively with `recursive: true` option', function(done) { - var dir = tree.getPath('home'); - var file = tree.getPath('home/bb/file1'); - watcher = watch(dir, { recursive: true }, function(evt, name) { +describe('options', () => { + describe('recursive', () => { + it('should watch recursively with `recursive: true` option', (done) => { + const dir = tree.getPath('home'); + const file = tree.getPath('home/bb/file1'); + watcher = watch(dir, { recursive: true }, (evt, name) => { if (file === name) { done(); } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify('home/bb/file1'); }); }); }); - describe('encoding', function() { - it('should throw on invalid encoding', function(done) { - var dir = tree.getPath('home/a'); + describe('encoding', () => { + it('should throw on invalid encoding', (done) => { + const dir = tree.getPath('home/a'); try { watcher = watch(dir, 'unknown'); } catch (e) { @@ -403,207 +407,204 @@ describe('options', function() { } }); - it('should accept options as an encoding string', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(dir, 'utf8', function(evt, name) { - assert.equal(name.toString(), fpath); + it('should accept options as an encoding string', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(dir, 'utf8', (evt, name) => { + assert.strictEqual(name.toString(), fpath); done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file); }); }); - it('should support buffer encoding', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(dir, 'buffer', function(evt, name) { - assert(Buffer.isBuffer(name), 'not a Buffer') - assert.equal(name.toString(), fpath); + it('should support buffer encoding', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(dir, 'buffer', (evt, name) => { + assert(Buffer.isBuffer(name), 'not a Buffer'); + assert.strictEqual(name.toString(), fpath); done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file); }); }); - it('should support base64 encoding', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(dir, 'base64', function(evt, name) { - assert.equal( + it('should support base64 encoding', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(dir, 'base64', (evt, name) => { + assert.strictEqual( name, Buffer.from(fpath).toString('base64'), 'wrong base64 encoding' ); done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file); }); }); - it('should support hex encoding', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var fpath = tree.getPath(file); - watcher = watch(dir, 'hex', function(evt, name) { - assert.equal( + it('should support hex encoding', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + const fpath = tree.getPath(file); + watcher = watch(dir, 'hex', (evt, name) => { + assert.strictEqual( name, Buffer.from(fpath).toString('hex'), 'wrong hex encoding' ); done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file); }); }); }); - describe('filter', function() { - it('should only watch filtered directories', function(done) { - var matchRegularDir = false; - var matchIgnoredDir = false; + describe('filter', () => { + it('should only watch filtered directories', (done) => { + let matchRegularDir = false; + let matchIgnoredDir = false; - var options = { + const options = { delay: 0, recursive: true, - filter: function(name) { + filter(name) { return !/deep_node_modules/.test(name); } }; - watcher = watch(tree.getPath('home'), options, function(evt, name) { + watcher = watch(tree.getPath('home'), options, (evt, name) => { if (/deep_node_modules/.test(name)) { matchIgnoredDir = true; } else { matchRegularDir = true; } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify('home/b/file1'); tree.modify('home/deep_node_modules/ma/file1'); - wait(function() { + wait(() => { assert(matchRegularDir, 'watch failed to detect regular file'); assert(!matchIgnoredDir, 'fail to ignore path `deep_node_modules`'); done(); - }, 100); + }); }); }); - it('should only report filtered files', function(done) { - var dir = tree.getPath('home'); - var file1 = 'home/bb/file1'; - var file2 = 'home/bb/file2'; + it('should only report filtered files', (done) => { + const dir = tree.getPath('home'); + const file1 = 'home/bb/file1'; + const file2 = 'home/bb/file2'; - var options = { + const options = { delay: 0, recursive: true, - filter: function(name) { + filter(name) { return /file2/.test(name); } - } + }; - var times = 0; - var matchIgnoredFile = false; - watcher = watch(dir, options, function(evt, name) { + let times = 0; + let matchIgnoredFile = false; + watcher = watch(dir, options, (evt, name) => { times++; if (name === tree.getPath(file1)) { matchIgnoredFile = true; } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file1); tree.modify(file2, 50); - wait(function() { - assert.equal(times, 1, 'should only report /home/bb/file2 once'); - assert.equal(matchIgnoredFile, false, 'home/bb/file1 should be ignored'); + wait(() => { + assert.strictEqual(times, 1, 'should only report /home/bb/file2 once'); + assert.strictEqual(matchIgnoredFile, false, 'home/bb/file1 should be ignored'); done(); - }, 100); + }); }); }); - it('should be able to filter with regexp', function(done) { - var dir = tree.getPath('home'); - var file1 = 'home/bb/file1'; - var file2 = 'home/bb/file2'; + it('should be able to filter with regexp', (done) => { + const dir = tree.getPath('home'); + const file1 = 'home/bb/file1'; + const file2 = 'home/bb/file2'; - var options = { + const options = { delay: 0, recursive: true, - filter: /file2/ - } + filter: /file2/ + }; - var times = 0; - var matchIgnoredFile = false; - watcher = watch(dir, options, function(evt, name) { + let times = 0; + let matchIgnoredFile = false; + watcher = watch(dir, options, (evt, name) => { times++; if (name === tree.getPath(file1)) { matchIgnoredFile = true; } }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file1); tree.modify(file2, 50); - wait(function() { + wait(() => { assert(times, 1, 'report file2'); assert(!matchIgnoredFile, 'home/bb/file1 should be ignored'); done(); - }, 100); + }); }); }); - it('should be able to skip subdirectories with `skip` flag', function(done) { - var home = tree.getPath('home'); - var options = { + it('should be able to skip subdirectories with `skip` flag', (done) => { + const home = tree.getPath('home'); + const options = { delay: 0, recursive: true, - filter: function(name, skip) { + filter(name, skip) { if (/\/deep_node_modules/.test(name)) return skip; } }; watcher = watch(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. + watcher.getWatchedPaths((paths) => { + hasNativeRecursive((supportRecursive) => { + const 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. - : tree.getAllDirectories().filter(function(name) { + // The deep_node_modules and all its subdirectories should not be watched + // with skip flag specified in the filter. + : tree.getAllDirectories().filter((name) => { return !/\/deep_node_modules/.test(name); }); - assert.deepStrictEqual( - watched.sort(), paths.sort() - ); - + assert.deepStrictEqual(watched.sort(), paths.sort()); done(); }); }); }); }); - describe('delay', function() { - it('should have delayed response', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var start; - watcher = watch(dir, { delay: 300 }, function(evt, name) { + describe('delay', () => { + it('should have delayed response', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + let start; + watcher = watch(dir, { delay: 300 }, () => { assert(Date.now() - start >= 300, 'delay not working'); done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { start = Date.now(); tree.modify(file); }); @@ -611,223 +612,214 @@ describe('options', function() { }); }); -describe('parameters', function() { - it('should throw error on non-existed file', function(done) { - var somedir = tree.getPath('home/somedir'); +describe('parameters', () => { + it('should throw error on non-existed file', (done) => { + const somedir = tree.getPath('home/somedir'); watcher = watch(somedir); - watcher.on('error', function(err) { + watcher.on('error', (err) => { if (err.message.includes('does not exist')) { - done() + done(); } - }) + }); }); - it('should accept filename as Buffer', function(done) { - var fpath = tree.getPath('home/a/file1'); - watcher = watch(Buffer.from(fpath), { delay: 0 }, function(evt, name) { - assert.equal(name, fpath); + it('should accept filename as Buffer', (done) => { + const fpath = tree.getPath('home/a/file1'); + watcher = watch(Buffer.from(fpath), { delay: 0 }, (evt, name) => { + assert.strictEqual(name, fpath); done(); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify('home/a/file1'); }); }); - it('should compose array of files or directories', function(done) { - var file1 = 'home/a/file1'; - var file2 = 'home/a/file2'; - var fpaths = [ + it('should compose array of files or directories', (done) => { + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + const fpaths = [ tree.getPath(file1), tree.getPath(file2) ]; - var times = 0; - watcher = watch(fpaths, { delay: 0 }, function(evt, name) { + let times = 0; + watcher = watch(fpaths, { delay: 0 }, (evt, name) => { if (fpaths.indexOf(name) !== -1) times++; - if (times === 2) done(); // calling done more than twice causes mocha test to fail + if (times === 2) done(); // calling done more than twice causes mocha test to fail }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file1); tree.modify(file2, 50); }); }); - it('should filter duplicate events for composed watcher', function(done) { - var home = 'home'; - var dir = 'home/a'; - var file1 = 'home/a/file1'; - var file2 = 'home/a/file2'; - var fpaths = [ + it('should filter duplicate events for composed watcher', (done) => { + const home = 'home'; + const dir = 'home/a'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + const fpaths = [ tree.getPath(home), tree.getPath(dir), tree.getPath(file1), tree.getPath(file2) ]; - var changes = []; - watcher = watch(fpaths, { delay: 100, recursive: true }, function(evt, name) { + const changes = []; + watcher = watch(fpaths, { delay: 100, recursive: true }, (evt, name) => { changes.push(name); }); - watcher.on('ready', function() { + watcher.on('ready', () => { tree.modify(file1); tree.modify(file2, 50); - wait(function() { + wait(() => { assert.deepStrictEqual( - changes, - [tree.getPath(file1), tree.getPath(file2)] + changes.sort(), + [tree.getPath(file1), tree.getPath(file2)].sort() ); done(); - }, 200); + }); }); }); }); -describe('watcher object', function() { - it('should using watcher object to watch', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var fpath = tree.getPath(file); +describe('watcher object', () => { + it('should using watcher object to watch', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + const fpath = tree.getPath(file); watcher = watch(dir, { delay: 0 }); - watcher.on('ready', function() { - watcher.on('change', function(evt, name) { - assert.equal(evt, 'update'); - assert.equal(name, fpath); + watcher.on('ready', () => { + watcher.on('change', (evt, name) => { + assert.strictEqual(evt, 'update'); + assert.strictEqual(name, fpath); done(); }); tree.modify(file); }); }); - describe('close()', function() { - it('should close a watcher using .close()', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var times = 0; + describe('close()', () => { + it('should close a watcher using .close()', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + let times = 0; watcher = watch(dir, { delay: 0 }); - watcher.on('change', function(evt, name) { + watcher.on('change', () => { times++; }); - watcher.on('ready', function() { - + watcher.on('ready', () => { watcher.close(); tree.modify(file); tree.modify(file, 100); - wait(function() { + wait(() => { assert(watcher.isClosed(), 'watcher should be closed'); - assert.equal(times, 0, 'failed to close the watcher'); + assert.strictEqual(times, 0, 'failed to close the watcher'); done(); - }, 150); + }); }); }); - it('should not watch after .close() is called', function(done) { - var dir = tree.getPath('home'); + it('should not watch after .close() is called', (done) => { + const dir = tree.getPath('home'); watcher = watch(dir, { delay: 0, recursive: true }); watcher.close(); - watcher.getWatchedPaths(function(dirs) { + watcher.getWatchedPaths((dirs) => { assert(dirs.length === 0); done(); }); }); - it('Do not emit after close', function(done) { - var dir = tree.getPath('home/a'); - var file = 'home/a/file1'; - var times = 0; + it('Do not emit after close', (done) => { + const dir = tree.getPath('home/a'); + const file = 'home/a/file1'; + let times = 0; watcher = watch(dir, { delay: 0 }); - watcher.on('change', function(evt, name) { + watcher.on('change', () => { times++; }); - watcher.on('ready', function() { - + watcher.on('ready', () => { watcher.close(); - var timer = setInterval(function() { + const timer = setInterval(() => { tree.modify(file); }); - wait(function() { + wait(() => { clearInterval(timer); assert(watcher.isClosed(), 'watcher should be closed'); - assert.equal(times, 0, 'failed to close the watcher'); + assert.strictEqual(times, 0, 'failed to close the watcher'); done(); - }, 100); + }); }); }); - }); - describe('getWatchedPaths()', function() { - it('should get all the watched paths', function(done) { - var home = tree.getPath('home'); + describe('getWatchedPaths()', () => { + it('should get all the watched paths', (done) => { + const home = tree.getPath('home'); watcher = watch(home, { delay: 0, recursive: true }); - watcher.getWatchedPaths(function(paths) { - hasNativeRecursive(function(supportRecursive) { - var watched = supportRecursive - // The home directory is the only one that's being watched - // if the recursive option is natively supported. + watcher.getWatchedPaths((paths) => { + hasNativeRecursive((supportRecursive) => { + const 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. + // Otherwise it should include all its subdirectories. : tree.getAllDirectories(); - assert.deepStrictEqual( - watched.sort(), paths.sort() - ); - + assert.deepStrictEqual(watched.sort(), paths.sort()); done(); }); }); }); - it('should get its parent path instead of the file itself', function(done) { - var file = tree.getPath('home/a/file1'); + it('should get its parent path instead of the file itself', (done) => { + const file = tree.getPath('home/a/file1'); // The parent path is actually being watched instead. - var parent = tree.getPath('home/a'); + const parent = tree.getPath('home/a'); watcher = watch(file, { delay: 0 }); - watcher.getWatchedPaths(function(paths) { + watcher.getWatchedPaths((paths) => { assert.deepStrictEqual([parent], paths); done(); }); }); - it('should work correctly with composed watcher', function(done) { - var a = tree.getPath('home/a'); + it('should work correctly with composed watcher', (done) => { + const a = tree.getPath('home/a'); - var b = tree.getPath('home/b'); - var file = tree.getPath('home/b/file1'); + const b = tree.getPath('home/b'); + const file = tree.getPath('home/b/file1'); - var nested = tree.getPath('home/deep_node_modules'); - var ma = tree.getPath('home/deep_node_modules/ma'); - var mb = tree.getPath('home/deep_node_modules/mb'); - var mc = tree.getPath('home/deep_node_modules/mc'); + const nested = tree.getPath('home/deep_node_modules'); + const ma = tree.getPath('home/deep_node_modules/ma'); + const mb = tree.getPath('home/deep_node_modules/mb'); + const mc = tree.getPath('home/deep_node_modules/mc'); watcher = watch([a, file, nested], { delay: 0, recursive: true }); - watcher.getWatchedPaths(function(paths) { - hasNativeRecursive(function(supportRecursive) { - var watched = supportRecursive + watcher.getWatchedPaths((paths) => { + hasNativeRecursive((supportRecursive) => { + const watched = supportRecursive ? [a, b, nested] : [a, b, nested, ma, mb, mc]; - assert.deepStrictEqual( - watched.sort(), paths.sort() - ); - + assert.deepStrictEqual(watched.sort(), paths.sort()); done(); }); }); diff --git a/test/utils/builder.js b/test/utils/builder.js index d23c040..36acb9b 100644 --- a/test/utils/builder.js +++ b/test/utils/builder.js @@ -1,64 +1,112 @@ -var fs = require('fs-extra'); -var path = require('path'); - -var structure = fs.readFileSync( - path.join(__dirname, './structure'), - 'utf-8' -); - -var code = structure - .split('\n') - .map(function(line) { - return { - indent: line.length - line.replace(/^\s+/,'').length, - type: /\/$/.test(line) ? 'dir': 'file', - text: line.replace(/^\s+|\s*\/\s*|\s+$/g, '') - } - }) +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const structure = fs.readFileSync(path.join(__dirname, './structure'), 'utf-8'); +const code = structure.split('\n').map((line) => ({ + indent: line.length - line.replace(/^\s+/, '').length, + type: /\/$/.test(line) ? 'dir' : 'file', + text: line.replace(/^\s+|\s*\/\s*|\s+$/g, '') +})); + +/** + * Join path segments + * @param {string[]} arr - Path segments + * @returns {string} + */ function join(arr) { return arr.join('/'); } +/** + * Transform parsed structure into path list + * @param {Array} arr - Parsed structure + * @returns {Array} + */ function transform(arr) { - var result = []; - var temp = []; - var indent = 0; - arr.forEach(function(line) { + const result = []; + const temp = []; + let indent = 0; + + arr.forEach((line) => { if (!line.text) { return; - } - else if (!line.indent) { + } else if (!line.indent) { temp.push(line.text); - result.push({type: line.type, text: join(temp) }); - } - else if (indent < line.indent) { + result.push({ type: line.type, text: join(temp) }); + } else if (indent < line.indent) { temp.push(line.text); result[result.length - 1].type = 'dir'; - result.push({type: line.type, text: join(temp) }); - } - else if (indent === line.indent) { + result.push({ type: line.type, text: join(temp) }); + } else if (indent === line.indent) { temp.pop(); temp.push(line.text); - result.push({type: line.type, text: join(temp) }); - } - else if(indent > line.indent) { + result.push({ type: line.type, text: join(temp) }); + } else if (indent > line.indent) { temp.pop(); temp.pop(); - temp.push(line.text) - result.push({type: line.type, text: join(temp) }); + temp.push(line.text); + result.push({ type: line.type, text: join(temp) }); } indent = line.indent; }); + return result; } -var transformed= transform(code); -var defaultTestPath= path.join(__dirname, '__TREE__'); +/** + * Ensure directory exists (like fs-extra's ensureDirSync) + * @param {string} dirPath - Directory path to ensure + */ +function ensureDirSync(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +/** + * Ensure file exists, creating parent directories if needed (like fs-extra's ensureFileSync) + * @param {string} filePath - File path to ensure + */ +function ensureFileSync(filePath) { + const dir = path.dirname(filePath); + ensureDirSync(dir); + // Only create if it doesn't exist + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, ''); + } +} -var delayTimers = []; +/** + * Remove file or directory recursively (like fs-extra's removeSync) + * @param {string} targetPath - Path to remove + */ +function removeSync(targetPath) { + fs.rmSync(targetPath, { recursive: true, force: true }); +} +/** + * Ensure symlink exists, creating parent directories if needed + * @param {string} srcPath - Source path + * @param {string} destPath - Destination path + */ +function ensureSymlinkSync(srcPath, destPath) { + const dir = path.dirname(destPath); + ensureDirSync(dir); + fs.symlinkSync(srcPath, destPath); +} + +const transformed = transform(code); +const defaultTestPath = path.join(__dirname, '__TREE__'); + +const delayTimers = []; + +/** + * Execute function with optional delay + * @param {Function} fn - Function to execute + * @param {number} [delay] - Delay in milliseconds + */ function maybeDelay(fn, delay) { if (delay) { delayTimers.push(setTimeout(fn, delay)); @@ -67,80 +115,137 @@ function maybeDelay(fn, delay) { } } +/** + * Clear all pending delay timers + */ function clearDelayTimers() { delayTimers.forEach(clearTimeout); delayTimers.length = 0; } +/** + * Create a test file tree builder + * @returns {Object} Tree builder interface + */ module.exports = function builder() { clearDelayTimers(); - var root = defaultTestPath; - transformed.forEach(function(line) { - var target = path.join(root, line.text) + const root = defaultTestPath; + + transformed.forEach((line) => { + const target = path.join(root, line.text); if (line.type === 'dir') { - fs.ensureDirSync(target); - } - else { - fs.ensureFileSync(target); + ensureDirSync(target); + } else { + ensureFileSync(target); } }); + return { - getPath: function(fpath, sub) { + /** + * Get full path for a relative path + * @param {string} fpath - Relative path + * @param {string} [sub] - Sub path + * @returns {string} + */ + getPath(fpath, sub) { return path.join(root, fpath, sub || ''); }, - modify: function(fpath, delay) { - var filePath = this.getPath(fpath); - maybeDelay(function() { + + /** + * Modify a file (append content) + * @param {string} fpath - File path + * @param {number} [delay] - Delay in milliseconds + */ + modify(fpath, delay) { + const filePath = this.getPath(fpath); + maybeDelay(() => { fs.appendFileSync(filePath, 'hello'); }, delay); }, - remove: function(fpath, delay) { - var filePath = this.getPath(fpath); - maybeDelay(function() { - fs.removeSync(filePath); + + /** + * Remove a file or directory + * @param {string} fpath - Path to remove + * @param {number} [delay] - Delay in milliseconds + */ + remove(fpath, delay) { + const filePath = this.getPath(fpath); + maybeDelay(() => { + removeSync(filePath); }, delay); }, - newFile: function(fpath, delay) { - var filePath = this.getPath(fpath); - maybeDelay(function() { - fs.ensureFileSync(filePath); + + /** + * Create a new file + * @param {string} fpath - File path + * @param {number} [delay] - Delay in milliseconds + */ + newFile(fpath, delay) { + const filePath = this.getPath(fpath); + maybeDelay(() => { + ensureFileSync(filePath); }, delay); }, - newRandomFiles: function(fpath, count) { - var names = []; - for (var i = 0; i < count; ++i) { - var name = Math.random().toString().substr(2); - var filePath = this.getPath(fpath, name); - fs.ensureFileSync(filePath); + + /** + * Create multiple random files + * @param {string} fpath - Directory path + * @param {number} count - Number of files to create + * @returns {string[]} Array of created file paths + */ + newRandomFiles(fpath, count) { + const names = []; + for (let i = 0; i < count; ++i) { + const name = Math.random().toString().slice(2); + const filePath = this.getPath(fpath, name); + ensureFileSync(filePath); names.push(path.join(fpath, name)); } return names; }, - newSymLink: function(src, dist) { - fs.ensureSymlinkSync( - this.getPath(src), - this.getPath(dist) - ); + + /** + * Create a symbolic link + * @param {string} src - Source path + * @param {string} dist - Destination path + */ + newSymLink(src, dist) { + ensureSymlinkSync(this.getPath(src), this.getPath(dist)); }, - newDir: function(fpath, delay) { - var filePath = this.getPath(fpath); - maybeDelay(function() { - fs.ensureDirSync(filePath); + + /** + * Create a new directory + * @param {string} fpath - Directory path + * @param {number} [delay] - Delay in milliseconds + */ + newDir(fpath, delay) { + const filePath = this.getPath(fpath); + maybeDelay(() => { + ensureDirSync(filePath); }, delay); }, - cleanup: function() { + + /** + * Clean up the test tree + */ + cleanup() { try { - fs.removeSync(root); + removeSync(root); } catch (e) { console.warn('cleanup failed.'); } }, - getAllDirectories: function() { + + /** + * Get all directories in the tree + * @returns {string[]} + */ + getAllDirectories() { function walk(dir) { - var ret = []; - fs.readdirSync(dir).forEach(function(d) { - var fpath = path.join(dir, d); + let ret = []; + fs.readdirSync(dir).forEach((d) => { + const fpath = path.join(dir, d); if (fs.statSync(fpath).isDirectory()) { ret.push(fpath); ret = ret.concat(walk(fpath)); @@ -150,5 +255,5 @@ module.exports = function builder() { } return walk(root); } - } -} + }; +};