├── .gitignore ├── test ├── utils │ ├── structure │ └── builder.js └── test.js ├── .github └── workflows │ └── ci.yml ├── package.json ├── LICENSE ├── lib ├── is.js ├── watch.d.ts ├── has-native-recursive.js └── watch.js ├── Changelog.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/utils/__TREE__ 3 | .nyc_output/ 4 | coverage/ 5 | package-lock.json 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /test/utils/structure: -------------------------------------------------------------------------------- 1 | home/ 2 | a/ 3 | file1 4 | file2 5 | b/ 6 | file1 7 | file2 8 | c/ 9 | bb/ 10 | file1 11 | file2 12 | d/ 13 | file1 14 | file2 15 | e/ 16 | file1 17 | file2 18 | sub/ 19 | deep_node_modules/ 20 | ma/ 21 | file1 22 | file2 23 | mb/ 24 | file1 25 | file2 26 | mc/ 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [macos-latest, ubuntu-latest] 16 | node-version: [14.x, 18.x, 20.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "A wrapper and enhancements for fs.watch", 3 | "license": "MIT", 4 | "name": "node-watch", 5 | "repository": { 6 | "url": "git://github.com/yuanchuan/node-watch.git", 7 | "type": "git" 8 | }, 9 | "keywords": [ 10 | "fs.watch", 11 | "watch", 12 | "watchfile" 13 | ], 14 | "version": "0.7.4", 15 | "bugs": { 16 | "url": "https://github.com/yuanchuan/node-watch/issues" 17 | }, 18 | "url": "https://github.com/yuanchuan/node-watch", 19 | "author": "yuanchuan (http://yuanchuan.name)", 20 | "main": "./lib/watch", 21 | "types": "./lib/watch.d.ts", 22 | "files": [ 23 | "lib/" 24 | ], 25 | "homepage": "https://github.com/yuanchuan/node-watch#readme", 26 | "scripts": { 27 | "test": "mocha test/test.js --exit --slow 500" 28 | }, 29 | "engines": { 30 | "node": ">=6" 31 | }, 32 | "devDependencies": { 33 | "fs-extra": "^7.0.1", 34 | "mocha": "^10.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012-2021 Yuan Chuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/is.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var os = require('os'); 4 | 5 | function matchObject(item, str) { 6 | return Object.prototype.toString.call(item) 7 | === '[object ' + str + ']'; 8 | } 9 | 10 | function checkStat(name, fn) { 11 | try { 12 | return fn(name); 13 | } catch (err) { 14 | if (/^(ENOENT|EPERM|EACCES)$/.test(err.code)) { 15 | if (err.code !== 'ENOENT') { 16 | console.warn('Warning: Cannot access %s', name); 17 | } 18 | return false; 19 | } 20 | throw err; 21 | } 22 | } 23 | 24 | var is = { 25 | nil: function(item) { 26 | return item == null; 27 | }, 28 | array: function(item) { 29 | return Array.isArray(item); 30 | }, 31 | emptyObject: function(item) { 32 | for (var key in item) { 33 | return false; 34 | } 35 | return true; 36 | }, 37 | buffer: function(item) { 38 | return Buffer.isBuffer(item); 39 | }, 40 | regExp: function(item) { 41 | return matchObject(item, 'RegExp'); 42 | }, 43 | string: function(item) { 44 | return matchObject(item, 'String'); 45 | }, 46 | func: function(item) { 47 | return typeof item === 'function'; 48 | }, 49 | number: function(item) { 50 | return matchObject(item, 'Number'); 51 | }, 52 | exists: function(name) { 53 | return fs.existsSync(name); 54 | }, 55 | file: function(name) { 56 | return checkStat(name, function(n) { 57 | return fs.statSync(n).isFile() 58 | }); 59 | }, 60 | samePath: function(a, b) { 61 | return path.resolve(a) === path.resolve(b); 62 | }, 63 | directory: function(name) { 64 | return checkStat(name, function(n) { 65 | return fs.statSync(n).isDirectory() 66 | }); 67 | }, 68 | symbolicLink: function(name) { 69 | return checkStat(name, function(n) { 70 | return fs.lstatSync(n).isSymbolicLink(); 71 | }); 72 | }, 73 | windows: function() { 74 | return os.platform() === 'win32'; 75 | } 76 | }; 77 | 78 | module.exports = is; 79 | -------------------------------------------------------------------------------- /lib/watch.d.ts: -------------------------------------------------------------------------------- 1 | import { FSWatcher } from 'fs'; 2 | 3 | /** 4 | * Watch for changes on `filename`, where filename is either a file or a directory. 5 | * The second argument is optional. 6 | * 7 | * If `options` is provided as a string, it specifies the encoding. 8 | * Otherwise `options` should be passed as an object. 9 | * 10 | * The listener callback gets two arguments, `(eventType, filePath)`, 11 | * which is the same with `fs.watch`. 12 | * `eventType` is either `update` or `remove`, 13 | * `filePath` is the name of the file which triggered the event. 14 | * 15 | * @param {Filename} filename File or directory to watch. 16 | * @param {Options|string} options 17 | * @param {Function} callback 18 | */ 19 | declare function watch(pathName: PathName): Watcher; 20 | declare function watch(pathName: PathName, options: Options) : Watcher; 21 | declare function watch(pathName: PathName, callback: Callback): Watcher; 22 | declare function watch(pathName: PathName, options: Options, callback: Callback): Watcher; 23 | 24 | type EventType = 'update' | 'remove'; 25 | type Callback = (eventType: EventType, filePath: string) => any; 26 | type PathName = string | Array; 27 | type FilterReturn = boolean | symbol; 28 | 29 | type Options = { 30 | /** 31 | * Indicates whether the process should continue to run 32 | * as long as files are being watched. 33 | * @default true 34 | */ 35 | persistent ?: boolean; 36 | 37 | /** 38 | * Indicates whether all subdirectories should be watched. 39 | * @default false 40 | */ 41 | recursive ?: boolean; 42 | 43 | /** 44 | * Specifies the character encoding to be used for the filename 45 | * passed to the listener. 46 | * @default 'utf8' 47 | */ 48 | encoding ?: string; 49 | 50 | /** 51 | * Only files which pass this filter (when it returns `true`) 52 | * will be sent to the listener. 53 | */ 54 | filter ?: RegExp | ((file: string, skip: symbol) => FilterReturn); 55 | 56 | /** 57 | * Delay time of the callback function. 58 | * @default 200 59 | */ 60 | delay ?: number; 61 | }; 62 | 63 | export declare interface Watcher extends FSWatcher { 64 | /** 65 | * Returns `true` if the watcher has been closed. 66 | */ 67 | isClosed(): boolean; 68 | 69 | /** 70 | * Returns all watched paths. 71 | */ 72 | getWatchedPaths(): Array; 73 | } 74 | 75 | export default watch; 76 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.7.4 4 | 5 | * Fix: add export to interface #128 (by @multivoltage) 6 | * Catch fs.watch exceptions #125 (by @campersau ) 7 | * Fix can't listener error event on incorrect file/directory #123 (by @leijuns) 8 | 9 |

10 | 11 | 12 | ## 0.7.3 13 | 14 | * Fixed the type definition of callback function. (by @xieyuheng) 15 | * Optimization to the guard function. (by @wmertens) 16 | * Switched to Github Actions for CI. 17 | 18 |

19 | 20 | 21 | ## 0.7.2 22 | 23 | * Reduce the released npm package size. 24 | 25 |

26 | 27 | 28 | ## 0.7.1 29 | 30 | * Don't normalize events for Windows or it might lose essential events. 31 | * Fix the functionality of the `.close()` method before watcher is ready. 32 | 33 | 34 |

35 | 36 | 37 | ## 0.7.0 38 | 39 | * Add an extra flag for skipping sub-directories inside filter function. 40 | 41 |

42 | 43 | 44 | ## 0.6.4 45 | 46 | * Fix `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` error for Node v14. 47 | 48 |

49 | 50 | 51 | ## 0.6.3 52 | 53 | * Types: Allow watching multiple files. 54 | 55 |

56 | 57 | 58 | ## 0.6.2 59 | 60 | * Detect temporary editor files more wisely in order to avoid side effects on Windows. 61 | 62 |

63 | 64 | 65 | ## 0.6.1 66 | 67 | * Add TypeScript support. 68 | * Fix race condition of `fs.exists` and `fs.stat`. 69 | * Prevent redundant events on Windows when creating file/directory. 70 | 71 |

72 | 73 | 74 | ## 0.6.0 75 | Special thanks to [Timo Tijhof](https://github.com/Krinkle) 76 | 77 | * Drop support for node < 6.0 78 | * Add `ready` event for the watcher. 79 | * Lots of bug fixed. 80 | 81 |

82 | 83 | 84 | ## 0.5.9 85 | * Fix function detection. 86 | * Emit `close` event after calling `.close()`. 87 | * Don't emit any events after close. 88 | * Change default `delay` to 200ms. 89 | 90 |

91 | 92 | 93 | ## 0.5.8 94 | * Fix async function detection. 95 | 96 |

97 | 98 | 99 | ## 0.5.7 100 | * Add `delay` option and set default to 100ms. 101 | 102 |

103 | 104 | 105 | ## 0.5.6 106 | * Fix recursive watch with filter option. 107 | 108 |

109 | 110 | 111 | ## 0.5.5 112 | * Remove duplicate events from a composed watcher. 113 | 114 |

115 | 116 | 117 | ## 0.5.4 118 | * Accept Buffer filename. 119 | * Add support for `encoding` option. 120 | 121 |

122 | 123 | 124 | ## 0.5.3 125 | * The `filter` option can be of either Function or RegExp type. 126 | 127 |

128 | 129 | 130 | ## 0.5.0 131 | * The `recursive` option is default to be `false`. 132 | * The callback function will always provide an event name. 133 | 134 |

135 | 136 | 137 | ## 0.4.0 138 | * Returns a [fs.FSWatcher](https://nodejs.org/api/fs.html#fs_class_fs_fswatcher) like object. 139 | -------------------------------------------------------------------------------- /lib/has-native-recursive.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var os = require('os'); 3 | var path = require('path'); 4 | var is = require('./is'); 5 | 6 | var IS_SUPPORT; 7 | var TEMP_DIR = os.tmpdir && os.tmpdir() 8 | || process.env.TMPDIR 9 | || process.env.TEMP 10 | || process.cwd(); 11 | 12 | function TempStack() { 13 | this.stack = []; 14 | } 15 | 16 | TempStack.prototype = { 17 | create: function(type, base) { 18 | var name = path.join(base, 19 | 'node-watch-' + Math.random().toString(16).substr(2) 20 | ); 21 | this.stack.push({ name: name, type: type }); 22 | return name; 23 | }, 24 | write: function(/* file */) { 25 | for (var i = 0; i < arguments.length; ++i) { 26 | fs.writeFileSync(arguments[i], ' '); 27 | } 28 | }, 29 | mkdir: function(/* dirs */) { 30 | for (var i = 0; i < arguments.length; ++i) { 31 | fs.mkdirSync(arguments[i]); 32 | } 33 | }, 34 | cleanup: function(fn) { 35 | try { 36 | var temp; 37 | while ((temp = this.stack.pop())) { 38 | var type = temp.type; 39 | var name = temp.name; 40 | if (type === 'file' && is.file(name)) { 41 | fs.unlinkSync(name); 42 | } 43 | else if (type === 'dir' && is.directory(name)) { 44 | fs.rmdirSync(name); 45 | } 46 | } 47 | } 48 | finally { 49 | if (is.func(fn)) fn(); 50 | } 51 | } 52 | }; 53 | 54 | var pending = false; 55 | 56 | module.exports = function hasNativeRecursive(fn) { 57 | if (!is.func(fn)) { 58 | return false; 59 | } 60 | if (IS_SUPPORT !== undefined) { 61 | return fn(IS_SUPPORT); 62 | } 63 | 64 | if (!pending) { 65 | pending = true; 66 | } 67 | // check again later 68 | else { 69 | return setTimeout(function() { 70 | hasNativeRecursive(fn); 71 | }, 300); 72 | } 73 | 74 | var stack = new TempStack(); 75 | var parent = stack.create('dir', TEMP_DIR); 76 | var child = stack.create('dir', parent); 77 | var file = stack.create('file', child); 78 | 79 | try { 80 | stack.mkdir(parent, child); 81 | } catch (e) { 82 | stack = new TempStack(); 83 | // try again under current directory 84 | TEMP_DIR = process.cwd(); 85 | parent = stack.create('dir', TEMP_DIR); 86 | child = stack.create('dir', parent); 87 | file = stack.create('file', child); 88 | stack.mkdir(parent, child); 89 | } 90 | 91 | var options = { recursive: true }; 92 | var watcher; 93 | 94 | try { 95 | watcher = fs.watch(parent, options); 96 | } catch (e) { 97 | if (e.code == 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') { 98 | return fn(IS_SUPPORT = false); 99 | } else { 100 | throw e; 101 | } 102 | } 103 | 104 | if (!watcher) { 105 | return false; 106 | } 107 | 108 | var timer = setTimeout(function() { 109 | watcher.close(); 110 | stack.cleanup(function() { 111 | fn(IS_SUPPORT = false); 112 | }); 113 | }, 200); 114 | 115 | watcher.on('change', function(evt, name) { 116 | if (path.basename(file) === path.basename(name)) { 117 | watcher.close(); 118 | clearTimeout(timer); 119 | stack.cleanup(function() { 120 | fn(IS_SUPPORT = true); 121 | }); 122 | } 123 | }); 124 | stack.write(file); 125 | } 126 | -------------------------------------------------------------------------------- /test/utils/builder.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var path = require('path'); 3 | 4 | var structure = fs.readFileSync( 5 | path.join(__dirname, './structure'), 6 | 'utf-8' 7 | ); 8 | 9 | var code = structure 10 | .split('\n') 11 | .map(function(line) { 12 | return { 13 | indent: line.length - line.replace(/^\s+/,'').length, 14 | type: /\/$/.test(line) ? 'dir': 'file', 15 | text: line.replace(/^\s+|\s*\/\s*|\s+$/g, '') 16 | } 17 | }) 18 | 19 | function join(arr) { 20 | return arr.join('/'); 21 | } 22 | 23 | function transform(arr) { 24 | var result = []; 25 | var temp = []; 26 | var indent = 0; 27 | arr.forEach(function(line) { 28 | if (!line.text) { 29 | return; 30 | } 31 | else if (!line.indent) { 32 | temp.push(line.text); 33 | result.push({type: line.type, text: join(temp) }); 34 | } 35 | else if (indent < line.indent) { 36 | temp.push(line.text); 37 | result[result.length - 1].type = 'dir'; 38 | result.push({type: line.type, text: join(temp) }); 39 | } 40 | else if (indent === line.indent) { 41 | temp.pop(); 42 | temp.push(line.text); 43 | result.push({type: line.type, text: join(temp) }); 44 | } 45 | else if(indent > line.indent) { 46 | temp.pop(); 47 | temp.pop(); 48 | temp.push(line.text) 49 | result.push({type: line.type, text: join(temp) }); 50 | } 51 | 52 | indent = line.indent; 53 | }); 54 | return result; 55 | } 56 | 57 | var transformed= transform(code); 58 | var defaultTestPath= path.join(__dirname, '__TREE__'); 59 | 60 | var delayTimers = []; 61 | 62 | function maybeDelay(fn, delay) { 63 | if (delay) { 64 | delayTimers.push(setTimeout(fn, delay)); 65 | } else { 66 | fn(); 67 | } 68 | } 69 | 70 | function clearDelayTimers() { 71 | delayTimers.forEach(clearTimeout); 72 | delayTimers.length = 0; 73 | } 74 | 75 | module.exports = function builder() { 76 | clearDelayTimers(); 77 | 78 | var root = defaultTestPath; 79 | transformed.forEach(function(line) { 80 | var target = path.join(root, line.text) 81 | if (line.type === 'dir') { 82 | fs.ensureDirSync(target); 83 | } 84 | else { 85 | fs.ensureFileSync(target); 86 | } 87 | }); 88 | return { 89 | getPath: function(fpath, sub) { 90 | return path.join(root, fpath, sub || ''); 91 | }, 92 | modify: function(fpath, delay) { 93 | var filePath = this.getPath(fpath); 94 | maybeDelay(function() { 95 | fs.appendFileSync(filePath, 'hello'); 96 | }, delay); 97 | }, 98 | remove: function(fpath, delay) { 99 | var filePath = this.getPath(fpath); 100 | maybeDelay(function() { 101 | fs.removeSync(filePath); 102 | }, delay); 103 | }, 104 | newFile: function(fpath, delay) { 105 | var filePath = this.getPath(fpath); 106 | maybeDelay(function() { 107 | fs.ensureFileSync(filePath); 108 | }, delay); 109 | }, 110 | newRandomFiles: function(fpath, count) { 111 | var names = []; 112 | for (var i = 0; i < count; ++i) { 113 | var name = Math.random().toString().substr(2); 114 | var filePath = this.getPath(fpath, name); 115 | fs.ensureFileSync(filePath); 116 | names.push(path.join(fpath, name)); 117 | } 118 | return names; 119 | }, 120 | newSymLink: function(src, dist) { 121 | fs.ensureSymlinkSync( 122 | this.getPath(src), 123 | this.getPath(dist) 124 | ); 125 | }, 126 | newDir: function(fpath, delay) { 127 | var filePath = this.getPath(fpath); 128 | maybeDelay(function() { 129 | fs.ensureDirSync(filePath); 130 | }, delay); 131 | }, 132 | cleanup: function() { 133 | try { 134 | fs.removeSync(root); 135 | } catch (e) { 136 | console.warn('cleanup failed.'); 137 | } 138 | }, 139 | getAllDirectories: function() { 140 | function walk(dir) { 141 | var ret = []; 142 | fs.readdirSync(dir).forEach(function(d) { 143 | var fpath = path.join(dir, d); 144 | if (fs.statSync(fpath).isDirectory()) { 145 | ret.push(fpath); 146 | ret = ret.concat(walk(fpath)); 147 | } 148 | }); 149 | return ret; 150 | } 151 | return walk(root); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-watch [![Status](https://github.com/yuanchuan/node-watch/actions/workflows/ci.yml/badge.svg)](https://github.com/yuanchuan/node-watch/actions/workflows/ci.yml/badge.svg) 2 | 3 | A wrapper and enhancements for [fs.watch](http://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener). 4 | 5 | [![NPM](https://nodei.co/npm/node-watch.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/node-watch.png/) 6 | 7 | 8 | ## Installation 9 | 10 | ```bash 11 | npm install node-watch 12 | ``` 13 | 14 | ## Example 15 | 16 | ```js 17 | var watch = require('node-watch'); 18 | 19 | watch('file_or_dir', { recursive: true }, function(evt, name) { 20 | console.log('%s changed.', name); 21 | }); 22 | ``` 23 | 24 | Now it's fast to watch **deep** directories on macOS and Windows, since the `recursive` option is natively supported except on Linux. 25 | 26 | ```js 27 | // watch the whole disk 28 | watch('/', { recursive: true }, console.log); 29 | ``` 30 | 31 | 32 | ## Why? 33 | 34 | * Some editors will generate temporary files which will cause the callback function to be triggered multiple times. 35 | * The callback function will only be triggered once on watching a single file. 36 | * Missing an option to watch a directory recursively. 37 | * Recursive watch is not supported on Linux or in older versions of nodejs. 38 | * Keep it simple, stupid. 39 | 40 | 41 | ## Options 42 | 43 | The usage and options of `node-watch` are compatible with [fs.watch](https://nodejs.org/dist/latest-v7.x/docs/api/fs.html#fs_fs_watch_filename_options_listener). 44 | * `persistent: Boolean` (default **true**) 45 | * `recursive: Boolean` (default **false**) 46 | * `encoding: String` (default **'utf8'**) 47 | 48 | **Extra options** 49 | 50 | * `filter: RegExp | Function` 51 | 52 | Return that matches the filter expression. 53 | 54 | ```js 55 | // filter with regular expression 56 | watch('./', { filter: /\.json$/ }); 57 | 58 | // filter with custom function 59 | watch('./', { filter: f => !/node_modules/.test(f) }); 60 | 61 | ``` 62 | 63 | Each file and directory will be passed to the filter to determine whether 64 | it will then be passed to the callback function. Like `Array.filter` does in `JavaScript`. 65 | There are three kinds of return values for filter function: 66 | 67 | * **`true`**: Will be passed to callback. 68 | * **`false`**: Will not be passed to callback. 69 | * **`skip`**: Same with `false`, and skip to watch all its subdirectories. 70 | 71 | On Linux, where the `recursive` option is not natively supported, 72 | it is more efficient to skip ignored directories by returning the `skip` flag: 73 | 74 | ```js 75 | watch('./', { 76 | recursive: true, 77 | filter(f, skip) { 78 | // skip node_modules 79 | if (/\/node_modules/.test(f)) return skip; 80 | // skip .git folder 81 | if (/\.git/.test(f)) return skip; 82 | // only watch for js files 83 | return /\.js$/.test(f); 84 | } 85 | }); 86 | 87 | ``` 88 | 89 | If you prefer glob patterns you can use [minimatch](https://www.npmjs.com/package/minimatch) or [picomatch](https://www.npmjs.com/package/picomatch) 90 | together with filter: 91 | 92 | ```js 93 | const pm = require('picomatch'); 94 | let isMatch = pm('*.js'); 95 | 96 | watch('./', { 97 | filter: f => isMatch(f) 98 | }); 99 | ``` 100 | 101 | * `delay: Number` (in ms, default **200**) 102 | 103 | Delay time of the callback function. 104 | 105 | ```js 106 | // log after 5 seconds 107 | watch('./', { delay: 5000 }, console.log); 108 | ``` 109 | 110 | ## Events 111 | 112 | The events provided by the callback function is either `update` or `remove`, which is less confusing to `fs.watch`'s `rename` or `change`. 113 | 114 | ```js 115 | watch('./', function(evt, name) { 116 | 117 | if (evt == 'update') { 118 | // on create or modify 119 | } 120 | 121 | if (evt == 'remove') { 122 | // on delete 123 | } 124 | 125 | }); 126 | ``` 127 | 128 | 129 | ## Watcher object 130 | 131 | The watch function returns a [fs.FSWatcher](https://nodejs.org/api/fs.html#fs_class_fs_fswatcher) like object as the same as `fs.watch` (>= v0.4.0). 132 | 133 | #### Watcher events 134 | 135 | ```js 136 | let watcher = watch('./', { recursive: true }); 137 | 138 | watcher.on('change', function(evt, name) { 139 | // callback 140 | }); 141 | 142 | watcher.on('error', function(err) { 143 | // handle error 144 | }); 145 | 146 | watcher.on('ready', function() { 147 | // the watcher is ready to respond to changes 148 | }); 149 | ``` 150 | 151 | #### Close 152 | 153 | ```js 154 | // close 155 | watcher.close(); 156 | 157 | // is closed? 158 | watcher.isClosed() 159 | ``` 160 | 161 | #### List of methods 162 | 163 | * `.on` 164 | * `.once` 165 | * `.emit` 166 | * `.close` 167 | * `.listeners` 168 | * `.setMaxListeners` 169 | * `.getMaxListeners` 170 | 171 | ##### Extra methods 172 | * `.isClosed` detect if the watcher is closed 173 | * `.getWatchedPaths` get all the watched paths 174 | 175 | 176 | ## Known issues 177 | 178 | **Windows, node < v4.2.5** 179 | 180 | * Failed to detect `remove` event 181 | * Failed to get deleted filename or directory name 182 | 183 | **MacOS, node 0.10.x** 184 | * Will emit double event if the directory name is of one single character. 185 | 186 | 187 | ## Misc 188 | 189 | #### 1. Watch multiple files or directories in one place 190 | ```js 191 | watch(['file1', 'file2'], console.log); 192 | ``` 193 | 194 | #### 2. Customize watch command line tool 195 | ```js 196 | #!/usr/bin/env node 197 | 198 | // https://github.com/nodejs/node-v0.x-archive/issues/3211 199 | require('epipebomb')(); 200 | 201 | let watcher = require('node-watch')( 202 | process.argv[2] || './', { recursive: true }, console.log 203 | ); 204 | 205 | process.on('SIGINT', watcher.close); 206 | ``` 207 | Monitoring chrome from disk: 208 | ```bash 209 | $ watch / | grep -i chrome 210 | ``` 211 | 212 | #### 3. Got ENOSPC error? 213 | 214 | If you get ENOSPC error, but you actually have free disk space - it means that your OS watcher limit is too low and you probably want to recursively watch a big tree of files. 215 | 216 | Follow this description to increase the limit: 217 | [https://confluence.jetbrains.com/display/IDEADEV/Inotify+Watches+Limit](https://confluence.jetbrains.com/display/IDEADEV/Inotify+Watches+Limit) 218 | 219 | 220 | ## Alternatives 221 | 222 | * [chokidar](https://github.com/paulmillr/chokidar) 223 | * [gaze](https://github.com/shama/gaze) 224 | * [mikeal/watch](https://github.com/mikeal/watch) 225 | 226 | ## Contributors 227 | 228 | Thanks goes to [all wonderful people](https://github.com/yuanchuan/node-watch/graphs/contributors) who have helped this project. 229 | 230 | ## License 231 | MIT 232 | 233 | Copyright (c) 2012-2021 [yuanchuan](https://github.com/yuanchuan) 234 | -------------------------------------------------------------------------------- /lib/watch.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var util = require('util'); 4 | var events = require('events'); 5 | 6 | var hasNativeRecursive = require('./has-native-recursive'); 7 | var is = require('./is'); 8 | 9 | var EVENT_UPDATE = 'update'; 10 | var EVENT_REMOVE = 'remove'; 11 | 12 | var SKIP_FLAG = Symbol('skip'); 13 | 14 | function hasDup(arr) { 15 | return arr.some(function(v, i, self) { 16 | return self.indexOf(v) !== i; 17 | }); 18 | } 19 | 20 | function unique(arr) { 21 | return arr.filter(function(v, i, self) { 22 | return self.indexOf(v) === i; 23 | }); 24 | } 25 | 26 | // One level flat 27 | function flat1(arr) { 28 | return arr.reduce(function(acc, v) { 29 | return acc.concat(v); 30 | }, []); 31 | } 32 | 33 | function assertEncoding(encoding) { 34 | if (encoding && encoding !== 'buffer' && !Buffer.isEncoding(encoding)) { 35 | throw new Error('Unknown encoding: ' + encoding); 36 | } 37 | } 38 | 39 | function guard(fn) { 40 | if (is.func(fn)) { 41 | return function(arg, action) { 42 | if (fn(arg, false)) action(); 43 | } 44 | } 45 | if (is.regExp(fn)) { 46 | return function(arg, action) { 47 | if (fn.test(arg)) action(); 48 | } 49 | } 50 | return function(arg, action) { 51 | action(); 52 | } 53 | } 54 | 55 | function composeMessage(names) { 56 | return names.map(function(n) { 57 | return is.exists(n) 58 | ? [EVENT_UPDATE, n] 59 | : [EVENT_REMOVE, n]; 60 | }); 61 | } 62 | 63 | function getMessages(cache) { 64 | var filtered = unique(cache); 65 | 66 | // Saving file from an editor? If so, assuming the 67 | // non-existed files in the cache are temporary files 68 | // generated by an editor and thus be filtered. 69 | var reg = /~$|^\.#|^##$/g; 70 | var hasSpecialChar = cache.some(function(c) { 71 | return reg.test(c); 72 | }); 73 | 74 | if (hasSpecialChar) { 75 | var dup = hasDup(cache.map(function(c) { 76 | return c.replace(reg, ''); 77 | })); 78 | if (dup) { 79 | filtered = filtered.filter(function(m) { 80 | return is.exists(m); 81 | }); 82 | } 83 | } 84 | 85 | return composeMessage(filtered); 86 | } 87 | 88 | function debounce(info, fn) { 89 | var timer, cache = []; 90 | var encoding = info.options.encoding; 91 | var delay = info.options.delay; 92 | if (!is.number(delay)) { 93 | delay = 200; 94 | } 95 | function handle() { 96 | getMessages(cache).forEach(function(msg) { 97 | msg[1] = Buffer.from(msg[1]); 98 | if (encoding !== 'buffer') { 99 | msg[1] = msg[1].toString(encoding); 100 | } 101 | fn.apply(null, msg); 102 | }); 103 | timer = null; 104 | cache = []; 105 | } 106 | return function(rawEvt, name) { 107 | cache.push(name); 108 | if (!timer) { 109 | timer = setTimeout(handle, delay); 110 | } 111 | } 112 | } 113 | 114 | function createDupsFilter() { 115 | var memo = {}; 116 | return function(fn) { 117 | return function(evt, name) { 118 | memo[evt + name] = [evt, name]; 119 | setTimeout(function() { 120 | Object.keys(memo).forEach(function(n) { 121 | fn.apply(null, memo[n]); 122 | }); 123 | memo = {}; 124 | }); 125 | } 126 | } 127 | } 128 | 129 | function tryWatch(watcher, dir, opts) { 130 | try { 131 | return fs.watch(dir, opts); 132 | } catch (e) { 133 | process.nextTick(function() { 134 | watcher.emit('error', e); 135 | }); 136 | } 137 | } 138 | 139 | function getSubDirectories(dir, fn, done = function() {}) { 140 | if (is.directory(dir)) { 141 | fs.readdir(dir, function(err, all) { 142 | if (err) { 143 | // don't throw permission errors. 144 | if (/^(EPERM|EACCES)$/.test(err.code)) { 145 | console.warn('Warning: Cannot access %s.', dir); 146 | } else { 147 | throw err; 148 | } 149 | } 150 | else { 151 | all.forEach(function(f) { 152 | var sdir = path.join(dir, f); 153 | if (is.directory(sdir)) fn(sdir); 154 | }); 155 | done(); 156 | } 157 | }); 158 | } else { 159 | done(); 160 | } 161 | } 162 | 163 | function semaphore(final) { 164 | var counter = 0; 165 | return function start() { 166 | counter++; 167 | return function stop() { 168 | counter--; 169 | if (counter === 0) final(); 170 | }; 171 | }; 172 | } 173 | 174 | function nullCounter() { 175 | return function nullStop() {}; 176 | } 177 | 178 | function shouldNotSkip(filePath, filter) { 179 | // watch it only if the filter is not function 180 | // or not being skipped explicitly. 181 | return !is.func(filter) || filter(filePath, SKIP_FLAG) !== SKIP_FLAG; 182 | } 183 | 184 | var deprecationWarning = util.deprecate( 185 | function() {}, 186 | '(node-watch) First param in callback function\ 187 | is replaced with event name since 0.5.0, use\ 188 | `(evt, filename) => {}` if you want to get the filename' 189 | ); 190 | 191 | function Watcher() { 192 | events.EventEmitter.call(this); 193 | this.watchers = {}; 194 | this._isReady = false; 195 | this._isClosed = false; 196 | } 197 | 198 | util.inherits(Watcher, events.EventEmitter); 199 | 200 | Watcher.prototype.expose = function() { 201 | var expose = {}; 202 | var self = this; 203 | var methods = [ 204 | 'on', 'emit', 'once', 205 | 'close', 'isClosed', 206 | 'listeners', 'setMaxListeners', 'getMaxListeners', 207 | 'getWatchedPaths' 208 | ]; 209 | methods.forEach(function(name) { 210 | expose[name] = function() { 211 | return self[name].apply(self, arguments); 212 | } 213 | }); 214 | return expose; 215 | } 216 | 217 | Watcher.prototype.isClosed = function() { 218 | return this._isClosed; 219 | } 220 | 221 | Watcher.prototype.close = function(fullPath) { 222 | var self = this; 223 | if (fullPath) { 224 | var watcher = this.watchers[fullPath]; 225 | if (watcher && watcher.close) { 226 | watcher.close(); 227 | delete self.watchers[fullPath]; 228 | } 229 | getSubDirectories(fullPath, function(fpath) { 230 | self.close(fpath); 231 | }); 232 | } 233 | else { 234 | Object.keys(self.watchers).forEach(function(fpath) { 235 | var watcher = self.watchers[fpath]; 236 | if (watcher && watcher.close) { 237 | watcher.close(); 238 | } 239 | }); 240 | this.watchers = {}; 241 | } 242 | // Do not close the Watcher unless all child watchers are closed. 243 | // https://github.com/yuanchuan/node-watch/issues/75 244 | if (is.emptyObject(self.watchers)) { 245 | // should emit once 246 | if (!this._isClosed) { 247 | this._isClosed = true; 248 | process.nextTick(emitClose, this); 249 | } 250 | } 251 | } 252 | 253 | Watcher.prototype.getWatchedPaths = function(fn) { 254 | if (is.func(fn)) { 255 | var self = this; 256 | if (self._isReady) { 257 | fn(Object.keys(self.watchers)); 258 | } else { 259 | self.on('ready', function() { 260 | fn(Object.keys(self.watchers)); 261 | }); 262 | } 263 | } 264 | } 265 | 266 | function emitReady(self) { 267 | if (!self._isReady) { 268 | self._isReady = true; 269 | // do not call emit for 'ready' until after watch() has returned, 270 | // so that consumer can call on(). 271 | process.nextTick(function () { 272 | self.emit('ready'); 273 | }); 274 | } 275 | } 276 | 277 | function emitClose(self) { 278 | self.emit('close'); 279 | } 280 | 281 | Watcher.prototype.add = function(watcher, info) { 282 | var self = this; 283 | info = info || { fpath: '' }; 284 | var watcherPath = path.resolve(info.fpath); 285 | this.watchers[watcherPath] = watcher; 286 | 287 | // Internal callback for handling fs.FSWatcher 'change' events 288 | var internalOnChange = function(rawEvt, rawName) { 289 | if (self.isClosed()) { 290 | return; 291 | } 292 | 293 | // normalise lack of name and convert to full path 294 | var name = rawName; 295 | if (is.nil(name)) { 296 | name = ''; 297 | } 298 | name = path.join(info.fpath, name); 299 | 300 | if (info.options.recursive) { 301 | hasNativeRecursive(function(has) { 302 | if (!has) { 303 | var fullPath = path.resolve(name); 304 | // remove watcher on removal 305 | if (!is.exists(name)) { 306 | self.close(fullPath); 307 | } 308 | // watch new created directory 309 | else { 310 | var shouldWatch = is.directory(name) 311 | && !self.watchers[fullPath] 312 | && shouldNotSkip(name, info.options.filter); 313 | 314 | if (shouldWatch) { 315 | self.watchDirectory(name, info.options); 316 | } 317 | } 318 | } 319 | }); 320 | } 321 | 322 | handlePublicEvents(rawEvt, name); 323 | }; 324 | 325 | // Debounced based on the 'delay' option 326 | var handlePublicEvents = debounce(info, function (evt, name) { 327 | // watch single file 328 | if (info.compareName) { 329 | if (info.compareName(name)) { 330 | self.emit('change', evt, name); 331 | } 332 | } 333 | // watch directory 334 | else { 335 | var filterGuard = guard(info.options.filter); 336 | filterGuard(name, function() { 337 | if (self.flag) self.flag = ''; 338 | else self.emit('change', evt, name); 339 | }); 340 | } 341 | }); 342 | 343 | watcher.on('error', function(err) { 344 | if (self.isClosed()) { 345 | return; 346 | } 347 | if (is.windows() && err.code === 'EPERM') { 348 | watcher.emit('change', EVENT_REMOVE, info.fpath && ''); 349 | self.flag = 'windows-error'; 350 | self.close(watcherPath); 351 | } else { 352 | self.emit('error', err); 353 | } 354 | }); 355 | 356 | watcher.on('change', internalOnChange); 357 | } 358 | 359 | Watcher.prototype.watchFile = function(file, options, fn) { 360 | var parent = path.join(file, '../'); 361 | var opts = Object.assign({}, options, { 362 | // no filter for single file 363 | filter: null, 364 | encoding: 'utf8' 365 | }); 366 | 367 | // no need to watch recursively 368 | delete opts.recursive; 369 | 370 | var watcher = tryWatch(this, parent, opts); 371 | if (!watcher) { 372 | return; 373 | } 374 | 375 | this.add(watcher, { 376 | type: 'file', 377 | fpath: parent, 378 | options: Object.assign({}, opts, { 379 | encoding: options.encoding 380 | }), 381 | compareName: function(n) { 382 | return is.samePath(n, file); 383 | } 384 | }); 385 | 386 | if (is.func(fn)) { 387 | if (fn.length === 1) deprecationWarning(); 388 | this.on('change', fn); 389 | } 390 | } 391 | 392 | Watcher.prototype.watchDirectory = function(dir, options, fn, counter = nullCounter) { 393 | var self = this; 394 | var done = counter(); 395 | hasNativeRecursive(function(has) { 396 | // always specify recursive 397 | options.recursive = !!options.recursive; 398 | // using utf8 internally 399 | var opts = Object.assign({}, options, { 400 | encoding: 'utf8' 401 | }); 402 | if (!has) { 403 | delete opts.recursive; 404 | } 405 | 406 | // check if it's closed before calling watch. 407 | if (self._isClosed) { 408 | done(); 409 | return self.close(); 410 | } 411 | 412 | var watcher = tryWatch(self, dir, opts); 413 | if (!watcher) { 414 | done(); 415 | return; 416 | } 417 | 418 | self.add(watcher, { 419 | type: 'dir', 420 | fpath: dir, 421 | options: options 422 | }); 423 | 424 | if (is.func(fn)) { 425 | if (fn.length === 1) deprecationWarning(); 426 | self.on('change', fn); 427 | } 428 | 429 | if (options.recursive && !has) { 430 | getSubDirectories(dir, function(d) { 431 | if (shouldNotSkip(d, options.filter)) { 432 | self.watchDirectory(d, options, null, counter); 433 | } 434 | }, counter()); 435 | } 436 | 437 | done(); 438 | }); 439 | } 440 | 441 | function composeWatcher(watchers) { 442 | var watcher = new Watcher(); 443 | var filterDups = createDupsFilter(); 444 | var counter = watchers.length; 445 | 446 | watchers.forEach(function(w) { 447 | w.on('change', filterDups(function(evt, name) { 448 | watcher.emit('change', evt, name); 449 | })); 450 | w.on('error', function(err) { 451 | watcher.emit('error', err); 452 | }); 453 | w.on('ready', function() { 454 | if (!(--counter)) { 455 | emitReady(watcher); 456 | } 457 | }); 458 | }); 459 | 460 | watcher.close = function() { 461 | watchers.forEach(function(w) { 462 | w.close(); 463 | }); 464 | process.nextTick(emitClose, watcher); 465 | } 466 | 467 | watcher.getWatchedPaths = function(fn) { 468 | if (is.func(fn)) { 469 | var promises = watchers.map(function(w) { 470 | return new Promise(function(resolve) { 471 | w.getWatchedPaths(resolve); 472 | }); 473 | }); 474 | Promise.all(promises).then(function(result) { 475 | var ret = unique(flat1(result)); 476 | fn(ret); 477 | }); 478 | } 479 | } 480 | 481 | return watcher.expose(); 482 | } 483 | 484 | function watch(fpath, options, fn) { 485 | var watcher = new Watcher(); 486 | 487 | if (is.buffer(fpath)) { 488 | fpath = fpath.toString(); 489 | } 490 | 491 | if (!is.array(fpath) && !is.exists(fpath)) { 492 | process.nextTick(function() { 493 | watcher.emit('error', 494 | new Error(fpath + ' does not exist.') 495 | ); 496 | }); 497 | } 498 | 499 | if (is.string(options)) { 500 | options = { 501 | encoding: options 502 | } 503 | } 504 | 505 | if (is.func(options)) { 506 | fn = options; 507 | options = {}; 508 | } 509 | 510 | if (arguments.length < 2) { 511 | options = {}; 512 | } 513 | 514 | if (options.encoding) { 515 | assertEncoding(options.encoding); 516 | } else { 517 | options.encoding = 'utf8'; 518 | } 519 | 520 | if (is.array(fpath)) { 521 | if (fpath.length === 1) { 522 | return watch(fpath[0], options, fn); 523 | } 524 | var filterDups = createDupsFilter(); 525 | return composeWatcher(unique(fpath).map(function(f) { 526 | var w = watch(f, options); 527 | if (is.func(fn)) { 528 | w.on('change', filterDups(fn)); 529 | } 530 | return w; 531 | })); 532 | } 533 | 534 | if (is.file(fpath)) { 535 | watcher.watchFile(fpath, options, fn); 536 | emitReady(watcher); 537 | } 538 | 539 | else if (is.directory(fpath)) { 540 | var counter = semaphore(function () { 541 | emitReady(watcher); 542 | }); 543 | watcher.watchDirectory(fpath, options, fn, counter); 544 | } 545 | 546 | return watcher.expose(); 547 | } 548 | 549 | module.exports = watch; 550 | module.exports.default = watch; 551 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Tree = require('./utils/builder'); 3 | var watch = require('../lib/watch'); 4 | var is = require('../lib/is'); 5 | var hasNativeRecursive = require('../lib/has-native-recursive'); 6 | 7 | var tree = Tree(); 8 | var watcher; 9 | 10 | beforeEach(function() { 11 | tree = Tree(); 12 | }); 13 | 14 | afterEach(function(done) { 15 | if (watcher && !watcher.isClosed()) { 16 | watcher.on('close', done); 17 | watcher.close(); 18 | } else { 19 | done(); 20 | } 21 | }); 22 | 23 | after(function() { 24 | if (tree) { 25 | tree.cleanup(); 26 | } 27 | }); 28 | 29 | function wait(fn, timeout) { 30 | try { 31 | fn(); 32 | } catch (error) { 33 | timeout -= 30; 34 | if (timeout >= 0) { 35 | setTimeout(function() { 36 | wait(fn, timeout); 37 | }, 30); 38 | } else { 39 | throw error; 40 | } 41 | } 42 | } 43 | 44 | describe('process events', function() { 45 | it('should emit `close` event', function(done) { 46 | var file = 'home/a/file1'; 47 | var fpath = tree.getPath(file); 48 | watcher = watch(fpath, function() {}); 49 | watcher.on('close', function() { 50 | done(); 51 | }); 52 | watcher.close(); 53 | }); 54 | 55 | it('should emit `ready` event when watching a file', function(done) { 56 | var file = 'home/a/file1'; 57 | var fpath = tree.getPath(file); 58 | watcher = watch(fpath); 59 | watcher.on('ready', function() { 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should emit `ready` event when watching a directory recursively', function(done) { 65 | var dir = tree.getPath('home'); 66 | watcher = watch(dir, { recursive: true }); 67 | watcher.on('ready', function() { 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should emit `ready` properly in a composed watcher', function(done) { 73 | var dir1 = tree.getPath('home/a'); 74 | var dir2 = tree.getPath('home/b'); 75 | var file = tree.getPath('home/b/file1'); 76 | watcher = watch([dir1, dir2, file], { recursive: true }); 77 | watcher.on('ready', function() { 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('watch for files', function() { 84 | it('should watch a single file and keep watching', function(done) { 85 | var times = 1; 86 | var file = 'home/a/file1'; 87 | var fpath = tree.getPath(file); 88 | watcher = watch(fpath, { delay: 0 }, function(evt, name) { 89 | assert.equal(fpath, name) 90 | if (times++ >= 3) { 91 | done(); 92 | } 93 | }); 94 | watcher.on('ready', function() { 95 | tree.modify(file); 96 | tree.modify(file, 100); 97 | tree.modify(file, 200); 98 | }); 99 | }); 100 | 101 | it('should watch files inside a directory', function(done) { 102 | var fpath = tree.getPath('home/a'); 103 | var stack = [ 104 | tree.getPath('home/a/file1'), 105 | tree.getPath('home/a/file2') 106 | ]; 107 | watcher = watch(fpath, { delay: 0 }, function(evt, name) { 108 | stack.splice(stack.indexOf(name), 1); 109 | if (!stack.length) done(); 110 | }); 111 | 112 | watcher.on('ready', function() { 113 | tree.modify('home/a/file1'); 114 | tree.modify('home/a/file2', 100); 115 | }); 116 | }); 117 | 118 | it('should ignore duplicate changes', function(done) { 119 | var file = 'home/a/file2'; 120 | var fpath = tree.getPath(file); 121 | var times = 0; 122 | watcher = watch(fpath, { delay: 200 }, function(evt, name) { 123 | if (fpath === name) times++; 124 | }); 125 | watcher.on('ready', function() { 126 | tree.modify(file); 127 | tree.modify(file, 100); 128 | tree.modify(file, 150); 129 | 130 | wait(function() { 131 | assert.equal(times, 1) 132 | done(); 133 | }, 250); 134 | }); 135 | }); 136 | 137 | it('should listen to new created files', function(done) { 138 | var home = tree.getPath('home'); 139 | var newfile1 = 'home/a/newfile' + Math.random(); 140 | var newfile2 = 'home/a/newfile' + Math.random(); 141 | var changes = []; 142 | watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { 143 | changes.push(name); 144 | }); 145 | watcher.on('ready', function() { 146 | tree.newFile(newfile1); 147 | tree.newFile(newfile2); 148 | wait(function() { 149 | // On windows it will report its parent directory along with the filename 150 | // https://github.com/yuanchuan/node-watch/issues/79 151 | if (is.windows()) { 152 | // Make sure new files are deteced 153 | assert.ok( 154 | changes.includes(tree.getPath(newfile1)) && 155 | changes.includes(tree.getPath(newfile2)) 156 | ); 157 | // It should only include new files and its parent directory 158 | // if there are more than 2 events 159 | if (changes.length > 2) { 160 | let accepts = [ 161 | tree.getPath(newfile1), 162 | tree.getPath(newfile2), 163 | tree.getPath('home/a') 164 | ]; 165 | changes.forEach(function(name) { 166 | assert.ok(accepts.includes(name), name + " should not be included"); 167 | }); 168 | } 169 | } else { 170 | assert.deepStrictEqual( 171 | changes, 172 | [tree.getPath(newfile1), tree.getPath(newfile2)] 173 | ); 174 | } 175 | done(); 176 | }, 100); 177 | }); 178 | }); 179 | 180 | it('should error when parent gets deleted before calling fs.watch', function(done) { 181 | var fpath = tree.getPath('home/a/file1'); 182 | watcher = watch(fpath, Object.defineProperty({}, 'test', { 183 | enumerable: true, 184 | get: function() { 185 | tree.remove('home/a'); 186 | return 'test'; 187 | } 188 | })); 189 | watcher.on('error', function() { 190 | done(); 191 | }); 192 | }); 193 | }); 194 | 195 | describe('watch for directories', function() { 196 | it('should watch directories inside a directory', function(done) { 197 | var home = tree.getPath('home'); 198 | var dir = tree.getPath('home/c'); 199 | var events = []; 200 | 201 | watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { 202 | if (name === dir) { 203 | events.push(evt); 204 | } 205 | }); 206 | watcher.on('ready', function() { 207 | tree.remove('home/c'); 208 | 209 | wait(function () { 210 | assert.deepStrictEqual( 211 | events, 212 | [ 'remove' ] 213 | ); 214 | done(); 215 | }, 400); 216 | }); 217 | }); 218 | 219 | it('should watch new created directories', function(done) { 220 | var home = tree.getPath('home'); 221 | watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { 222 | if (name === tree.getPath('home/new/file1')) { 223 | done(); 224 | } 225 | }); 226 | watcher.on('ready', function() { 227 | // newFile() will create the 'new/' directory and the 'new/file1' file, 228 | // but, only the creation of the directory is observed. 229 | // Because of that, there will only be one event for file1, when it 230 | // is modified, not when it is created. 231 | tree.newFile('home/new/file1'); 232 | tree.modify('home/new/file1', 100); 233 | }); 234 | }); 235 | 236 | it('should not watch new created directories which are being skipped in the filter', function(done) { 237 | var home = tree.getPath('home'); 238 | var options = { 239 | delay: 0, 240 | recursive: true, 241 | filter: function(filePath, skip) { 242 | if (/ignored/.test(filePath)) return skip; 243 | return true; 244 | } 245 | } 246 | 247 | watcher = watch(home, options, function(evt, name) { 248 | assert.fail("event detect", name); 249 | }); 250 | 251 | watcher.on('ready', function() { 252 | tree.newFile('home/ignored/file'); 253 | tree.modify('home/ignored/file', 100); 254 | wait(done, 150); 255 | }); 256 | }); 257 | 258 | it('should keep watching after removal of sub directory', function(done) { 259 | var home = tree.getPath('home'); 260 | var file1 = tree.getPath('home/e/file1'); 261 | var file2 = tree.getPath('home/e/file2'); 262 | var dir = tree.getPath('home/e/sub'); 263 | var events = []; 264 | watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) { 265 | if (name === dir || name === file1 || name === file2) { 266 | events.push(name); 267 | } 268 | }); 269 | watcher.on('ready', function() { 270 | tree.remove('home/e/sub', 50); 271 | tree.modify('home/e/file1', 100); 272 | tree.modify('home/e/file2', 200); 273 | 274 | wait(function() { 275 | assert.deepStrictEqual(events, [dir, file1, file2]); 276 | done(); 277 | }, 300); 278 | }); 279 | }); 280 | 281 | it('should watch new directories without delay', function(done) { 282 | var home = tree.getPath('home'); 283 | var events = []; 284 | watcher = watch(home, { delay: 200, recursive: true }, function(evt, name) { 285 | if (name === tree.getPath('home/new/file1')) { 286 | events.push(evt); 287 | } 288 | }); 289 | watcher.on('ready', function() { 290 | tree.newFile('home/new/file1'); 291 | tree.modify('home/new/file1', 50); 292 | tree.modify('home/new/file1', 100); 293 | wait(function() { 294 | assert.deepStrictEqual(events, ['update']); 295 | done(); 296 | }, 350); 297 | }); 298 | }); 299 | 300 | it('should error when directory gets deleted before calling fs.watch', function(done) { 301 | var dir = 'home/c'; 302 | var fpath = tree.getPath(dir); 303 | watcher = watch(fpath, Object.defineProperty({}, 'test', { 304 | enumerable: true, 305 | get: function() { 306 | tree.remove(dir); 307 | return 'test'; 308 | } 309 | })); 310 | watcher.on('error', function() { 311 | done(); 312 | }); 313 | }); 314 | }); 315 | 316 | describe('file events', function() { 317 | it('should identify `remove` event', function(done) { 318 | var file = 'home/a/file1'; 319 | var fpath = tree.getPath(file); 320 | watcher = watch(fpath, function(evt, name) { 321 | if (evt === 'remove' && name === fpath) done(); 322 | }); 323 | watcher.on('ready', function() { 324 | tree.remove(file); 325 | }); 326 | }); 327 | 328 | it('should identify `remove` event on directory', function(done) { 329 | var dir = 'home/a'; 330 | var home = tree.getPath('home'); 331 | var fpath = tree.getPath(dir); 332 | watcher = watch(home, function(evt, name) { 333 | if (evt === 'remove' && name === fpath) done(); 334 | }); 335 | watcher.on('ready', function() { 336 | tree.remove(dir); 337 | }); 338 | }); 339 | 340 | it('should be able to handle many events on deleting', function(done) { 341 | var dir = 'home/a'; 342 | var fpath = tree.getPath(dir); 343 | var names = tree.newRandomFiles(dir, 300); 344 | 345 | var count = 0; 346 | watcher = watch(fpath, function(evt, name) { 347 | count += 1; 348 | if (count == names.length) done(); 349 | }); 350 | 351 | watcher.on('ready', function() { 352 | names.forEach(tree.remove.bind(tree)); 353 | }); 354 | }); 355 | 356 | it('should identify `update` event', function(done) { 357 | var file = 'home/a/file1'; 358 | var fpath = tree.getPath(file); 359 | watcher = watch(fpath, function(evt, name) { 360 | if (evt === 'update' && name === fpath) done(); 361 | }); 362 | watcher.on('ready', function() { 363 | tree.modify(file); 364 | }); 365 | }); 366 | 367 | it('should report `update` on new files', function(done) { 368 | var dir = tree.getPath('home/a'); 369 | var file = 'home/a/newfile' + Date.now(); 370 | var fpath = tree.getPath(file); 371 | watcher = watch(dir, function(evt, name) { 372 | if (evt === 'update' && name === fpath) done(); 373 | }); 374 | watcher.on('ready', function() { 375 | tree.newFile(file); 376 | }); 377 | }); 378 | }); 379 | 380 | describe('options', function() { 381 | describe('recursive', function() { 382 | it('should watch recursively with `recursive: true` option', function(done) { 383 | var dir = tree.getPath('home'); 384 | var file = tree.getPath('home/bb/file1'); 385 | watcher = watch(dir, { recursive: true }, function(evt, name) { 386 | if (file === name) { 387 | done(); 388 | } 389 | }); 390 | watcher.on('ready', function() { 391 | tree.modify('home/bb/file1'); 392 | }); 393 | }); 394 | }); 395 | 396 | describe('encoding', function() { 397 | it('should throw on invalid encoding', function(done) { 398 | var dir = tree.getPath('home/a'); 399 | try { 400 | watcher = watch(dir, 'unknown'); 401 | } catch (e) { 402 | done(); 403 | } 404 | }); 405 | 406 | it('should accept options as an encoding string', function(done) { 407 | var dir = tree.getPath('home/a'); 408 | var file = 'home/a/file1'; 409 | var fpath = tree.getPath(file); 410 | watcher = watch(dir, 'utf8', function(evt, name) { 411 | assert.equal(name.toString(), fpath); 412 | done(); 413 | }); 414 | watcher.on('ready', function() { 415 | tree.modify(file); 416 | }); 417 | }); 418 | 419 | it('should support buffer encoding', function(done) { 420 | var dir = tree.getPath('home/a'); 421 | var file = 'home/a/file1'; 422 | var fpath = tree.getPath(file); 423 | watcher = watch(dir, 'buffer', function(evt, name) { 424 | assert(Buffer.isBuffer(name), 'not a Buffer') 425 | assert.equal(name.toString(), fpath); 426 | done(); 427 | }); 428 | watcher.on('ready', function() { 429 | tree.modify(file); 430 | }); 431 | }); 432 | 433 | it('should support base64 encoding', function(done) { 434 | var dir = tree.getPath('home/a'); 435 | var file = 'home/a/file1'; 436 | var fpath = tree.getPath(file); 437 | watcher = watch(dir, 'base64', function(evt, name) { 438 | assert.equal( 439 | name, 440 | Buffer.from(fpath).toString('base64'), 441 | 'wrong base64 encoding' 442 | ); 443 | done(); 444 | }); 445 | watcher.on('ready', function() { 446 | tree.modify(file); 447 | }); 448 | }); 449 | 450 | it('should support hex encoding', function(done) { 451 | var dir = tree.getPath('home/a'); 452 | var file = 'home/a/file1'; 453 | var fpath = tree.getPath(file); 454 | watcher = watch(dir, 'hex', function(evt, name) { 455 | assert.equal( 456 | name, 457 | Buffer.from(fpath).toString('hex'), 458 | 'wrong hex encoding' 459 | ); 460 | done(); 461 | }); 462 | watcher.on('ready', function() { 463 | tree.modify(file); 464 | }); 465 | }); 466 | }); 467 | 468 | describe('filter', function() { 469 | it('should only watch filtered directories', function(done) { 470 | var matchRegularDir = false; 471 | var matchIgnoredDir = false; 472 | 473 | var options = { 474 | delay: 0, 475 | recursive: true, 476 | filter: function(name) { 477 | return !/deep_node_modules/.test(name); 478 | } 479 | }; 480 | 481 | watcher = watch(tree.getPath('home'), options, function(evt, name) { 482 | if (/deep_node_modules/.test(name)) { 483 | matchIgnoredDir = true; 484 | } else { 485 | matchRegularDir = true; 486 | } 487 | }); 488 | watcher.on('ready', function() { 489 | tree.modify('home/b/file1'); 490 | tree.modify('home/deep_node_modules/ma/file1'); 491 | 492 | wait(function() { 493 | assert(matchRegularDir, 'watch failed to detect regular file'); 494 | assert(!matchIgnoredDir, 'fail to ignore path `deep_node_modules`'); 495 | done(); 496 | }, 100); 497 | }); 498 | }); 499 | 500 | it('should only report filtered files', function(done) { 501 | var dir = tree.getPath('home'); 502 | var file1 = 'home/bb/file1'; 503 | var file2 = 'home/bb/file2'; 504 | 505 | var options = { 506 | delay: 0, 507 | recursive: true, 508 | filter: function(name) { 509 | return /file2/.test(name); 510 | } 511 | } 512 | 513 | var times = 0; 514 | var matchIgnoredFile = false; 515 | watcher = watch(dir, options, function(evt, name) { 516 | times++; 517 | if (name === tree.getPath(file1)) { 518 | matchIgnoredFile = true; 519 | } 520 | }); 521 | watcher.on('ready', function() { 522 | tree.modify(file1); 523 | tree.modify(file2, 50); 524 | 525 | wait(function() { 526 | assert.equal(times, 1, 'should only report /home/bb/file2 once'); 527 | assert.equal(matchIgnoredFile, false, 'home/bb/file1 should be ignored'); 528 | done(); 529 | }, 100); 530 | }); 531 | }); 532 | 533 | it('should be able to filter with regexp', function(done) { 534 | var dir = tree.getPath('home'); 535 | var file1 = 'home/bb/file1'; 536 | var file2 = 'home/bb/file2'; 537 | 538 | var options = { 539 | delay: 0, 540 | recursive: true, 541 | filter: /file2/ 542 | } 543 | 544 | var times = 0; 545 | var matchIgnoredFile = false; 546 | watcher = watch(dir, options, function(evt, name) { 547 | times++; 548 | if (name === tree.getPath(file1)) { 549 | matchIgnoredFile = true; 550 | } 551 | }); 552 | watcher.on('ready', function() { 553 | tree.modify(file1); 554 | tree.modify(file2, 50); 555 | 556 | wait(function() { 557 | assert(times, 1, 'report file2'); 558 | assert(!matchIgnoredFile, 'home/bb/file1 should be ignored'); 559 | done(); 560 | }, 100); 561 | }); 562 | }); 563 | 564 | it('should be able to skip subdirectories with `skip` flag', function(done) { 565 | var home = tree.getPath('home'); 566 | var options = { 567 | delay: 0, 568 | recursive: true, 569 | filter: function(name, skip) { 570 | if (/\/deep_node_modules/.test(name)) return skip; 571 | } 572 | }; 573 | watcher = watch(home, options); 574 | 575 | watcher.getWatchedPaths(function(paths) { 576 | hasNativeRecursive(function(supportRecursive) { 577 | var watched = supportRecursive 578 | // The skip flag has no effect to the platforms which support recursive option, 579 | // so the home directory is the only one that's in the watching list. 580 | ? [home] 581 | // The deep_node_modules and all its subdirectories should not be watched 582 | // with skip flag specified in the filter. 583 | : tree.getAllDirectories().filter(function(name) { 584 | return !/\/deep_node_modules/.test(name); 585 | }); 586 | 587 | assert.deepStrictEqual( 588 | watched.sort(), paths.sort() 589 | ); 590 | 591 | done(); 592 | }); 593 | }); 594 | }); 595 | }); 596 | 597 | describe('delay', function() { 598 | it('should have delayed response', function(done) { 599 | var dir = tree.getPath('home/a'); 600 | var file = 'home/a/file1'; 601 | var start; 602 | watcher = watch(dir, { delay: 300 }, function(evt, name) { 603 | assert(Date.now() - start >= 300, 'delay not working'); 604 | done(); 605 | }); 606 | watcher.on('ready', function() { 607 | start = Date.now(); 608 | tree.modify(file); 609 | }); 610 | }); 611 | }); 612 | }); 613 | 614 | describe('parameters', function() { 615 | it('should throw error on non-existed file', function(done) { 616 | var somedir = tree.getPath('home/somedir'); 617 | watcher = watch(somedir); 618 | watcher.on('error', function(err) { 619 | if (err.message.includes('does not exist')) { 620 | done() 621 | } 622 | }) 623 | }); 624 | 625 | it('should accept filename as Buffer', function(done) { 626 | var fpath = tree.getPath('home/a/file1'); 627 | watcher = watch(Buffer.from(fpath), { delay: 0 }, function(evt, name) { 628 | assert.equal(name, fpath); 629 | done(); 630 | }); 631 | watcher.on('ready', function() { 632 | tree.modify('home/a/file1'); 633 | }); 634 | }); 635 | 636 | it('should compose array of files or directories', function(done) { 637 | var file1 = 'home/a/file1'; 638 | var file2 = 'home/a/file2'; 639 | var fpaths = [ 640 | tree.getPath(file1), 641 | tree.getPath(file2) 642 | ]; 643 | 644 | var times = 0; 645 | watcher = watch(fpaths, { delay: 0 }, function(evt, name) { 646 | if (fpaths.indexOf(name) !== -1) times++; 647 | if (times === 2) done(); // calling done more than twice causes mocha test to fail 648 | }); 649 | 650 | watcher.on('ready', function() { 651 | tree.modify(file1); 652 | tree.modify(file2, 50); 653 | }); 654 | }); 655 | 656 | it('should filter duplicate events for composed watcher', function(done) { 657 | var home = 'home'; 658 | var dir = 'home/a'; 659 | var file1 = 'home/a/file1'; 660 | var file2 = 'home/a/file2'; 661 | var fpaths = [ 662 | tree.getPath(home), 663 | tree.getPath(dir), 664 | tree.getPath(file1), 665 | tree.getPath(file2) 666 | ]; 667 | 668 | var changes = []; 669 | watcher = watch(fpaths, { delay: 100, recursive: true }, function(evt, name) { 670 | changes.push(name); 671 | }); 672 | 673 | watcher.on('ready', function() { 674 | tree.modify(file1); 675 | tree.modify(file2, 50); 676 | 677 | wait(function() { 678 | assert.deepStrictEqual( 679 | changes, 680 | [tree.getPath(file1), tree.getPath(file2)] 681 | ); 682 | done(); 683 | }, 200); 684 | }); 685 | }); 686 | }); 687 | 688 | describe('watcher object', function() { 689 | it('should using watcher object to watch', function(done) { 690 | var dir = tree.getPath('home/a'); 691 | var file = 'home/a/file1'; 692 | var fpath = tree.getPath(file); 693 | 694 | watcher = watch(dir, { delay: 0 }); 695 | watcher.on('ready', function() { 696 | watcher.on('change', function(evt, name) { 697 | assert.equal(evt, 'update'); 698 | assert.equal(name, fpath); 699 | done(); 700 | }); 701 | tree.modify(file); 702 | }); 703 | }); 704 | 705 | describe('close()', function() { 706 | it('should close a watcher using .close()', function(done) { 707 | var dir = tree.getPath('home/a'); 708 | var file = 'home/a/file1'; 709 | var times = 0; 710 | watcher = watch(dir, { delay: 0 }); 711 | watcher.on('change', function(evt, name) { 712 | times++; 713 | }); 714 | watcher.on('ready', function() { 715 | 716 | watcher.close(); 717 | 718 | tree.modify(file); 719 | tree.modify(file, 100); 720 | 721 | wait(function() { 722 | assert(watcher.isClosed(), 'watcher should be closed'); 723 | assert.equal(times, 0, 'failed to close the watcher'); 724 | done(); 725 | }, 150); 726 | }); 727 | }); 728 | 729 | it('should not watch after .close() is called', function(done) { 730 | var dir = tree.getPath('home'); 731 | watcher = watch(dir, { delay: 0, recursive: true }); 732 | watcher.close(); 733 | 734 | watcher.getWatchedPaths(function(dirs) { 735 | assert(dirs.length === 0); 736 | done(); 737 | }); 738 | }); 739 | 740 | it('Do not emit after close', function(done) { 741 | var dir = tree.getPath('home/a'); 742 | var file = 'home/a/file1'; 743 | var times = 0; 744 | watcher = watch(dir, { delay: 0 }); 745 | watcher.on('change', function(evt, name) { 746 | times++; 747 | }); 748 | watcher.on('ready', function() { 749 | 750 | watcher.close(); 751 | 752 | var timer = setInterval(function() { 753 | tree.modify(file); 754 | }); 755 | 756 | wait(function() { 757 | clearInterval(timer); 758 | assert(watcher.isClosed(), 'watcher should be closed'); 759 | assert.equal(times, 0, 'failed to close the watcher'); 760 | done(); 761 | }, 100); 762 | }); 763 | }); 764 | 765 | }); 766 | 767 | describe('getWatchedPaths()', function() { 768 | it('should get all the watched paths', function(done) { 769 | var home = tree.getPath('home'); 770 | watcher = watch(home, { 771 | delay: 0, 772 | recursive: true 773 | }); 774 | watcher.getWatchedPaths(function(paths) { 775 | hasNativeRecursive(function(supportRecursive) { 776 | var watched = supportRecursive 777 | // The home directory is the only one that's being watched 778 | // if the recursive option is natively supported. 779 | ? [home] 780 | // Otherwise it should include all its subdirectories. 781 | : tree.getAllDirectories(); 782 | 783 | assert.deepStrictEqual( 784 | watched.sort(), paths.sort() 785 | ); 786 | 787 | done(); 788 | }); 789 | }); 790 | }); 791 | 792 | it('should get its parent path instead of the file itself', function(done) { 793 | var file = tree.getPath('home/a/file1'); 794 | // The parent path is actually being watched instead. 795 | var parent = tree.getPath('home/a'); 796 | 797 | watcher = watch(file, { delay: 0 }); 798 | 799 | watcher.getWatchedPaths(function(paths) { 800 | assert.deepStrictEqual([parent], paths); 801 | done(); 802 | }); 803 | }); 804 | 805 | it('should work correctly with composed watcher', function(done) { 806 | var a = tree.getPath('home/a'); 807 | 808 | var b = tree.getPath('home/b'); 809 | var file = tree.getPath('home/b/file1'); 810 | 811 | var nested = tree.getPath('home/deep_node_modules'); 812 | var ma = tree.getPath('home/deep_node_modules/ma'); 813 | var mb = tree.getPath('home/deep_node_modules/mb'); 814 | var mc = tree.getPath('home/deep_node_modules/mc'); 815 | 816 | watcher = watch([a, file, nested], { 817 | delay: 0, 818 | recursive: true 819 | }); 820 | 821 | watcher.getWatchedPaths(function(paths) { 822 | hasNativeRecursive(function(supportRecursive) { 823 | var watched = supportRecursive 824 | ? [a, b, nested] 825 | : [a, b, nested, ma, mb, mc]; 826 | 827 | assert.deepStrictEqual( 828 | watched.sort(), paths.sort() 829 | ); 830 | 831 | done(); 832 | }); 833 | }); 834 | }); 835 | }); 836 | }); 837 | --------------------------------------------------------------------------------