├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── automerge.yml │ └── bevry.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json └── source ├── index.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2018 September 26 2 | # https://github.com/bevry/base 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | indent_style = tab 12 | 13 | [{*.mk,*.py}] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.md] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [{*.json,*.lsrules,*.yml,*.bowerrc,*.babelrc}] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [{*.json,*.lsrules}] 26 | insert_final_newline = true 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [balupton] 2 | patreon: bevry 3 | open_collective: bevry 4 | ko_fi: balupton 5 | liberapay: bevry 6 | custom: ['https://bevry.me/fund'] 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: sunday 8 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | 'on': 3 | - pull_request 4 | jobs: 5 | automerge: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 10 | with: 11 | github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /.github/workflows/bevry.yml: -------------------------------------------------------------------------------- 1 | name: bevry 2 | 'on': 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: 10 | - ubuntu-latest 11 | - macos-latest 12 | - windows-latest 13 | node: 14 | - '10' 15 | - '12' 16 | - '14' 17 | - '16' 18 | runs-on: ${{ matrix.os }} 19 | continue-on-error: ${{ contains('macos-latest windows-latest', matrix.os) }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Install desired Node.js version 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: '14' 26 | - run: npm run our:setup 27 | - run: npm run our:compile 28 | - run: npm run our:verify 29 | - name: Install targeted Node.js 30 | if: ${{ matrix.node != 14 }} 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node }} 34 | - run: npm test 35 | publish: 36 | if: ${{ github.event_name == 'push' }} 37 | needs: test 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | - name: Install desired Node.js version 42 | uses: actions/setup-node@v2 43 | with: 44 | node-version: '14' 45 | - run: npm run our:setup 46 | - run: npm run our:compile 47 | - run: npm run our:meta 48 | - name: publish to npm 49 | uses: bevry-actions/npm@v1.1.0 50 | with: 51 | npmAuthToken: ${{ secrets.NPM_AUTH_TOKEN }} 52 | npmBranchTag: ':next' 53 | - name: publish to surge 54 | uses: bevry-actions/surge@v1.0.3 55 | with: 56 | surgeLogin: ${{ secrets.SURGE_LOGIN }} 57 | surgeToken: ${{ secrets.SURGE_TOKEN }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2020 June 3 2 | # https://github.com/bevry/base 3 | 4 | # System Files 5 | **/.DS_Store 6 | 7 | # Temp Files 8 | **/.docpad.db 9 | **/*.log 10 | **/*.cpuprofile 11 | **/*.heapsnapshot 12 | 13 | # Editor Files 14 | .c9/ 15 | .vscode/ 16 | 17 | # Yarn Files 18 | .yarn/* 19 | !.yarn/releases 20 | !.yarn/plugins 21 | !.yarn/sdks 22 | !.yarn/versions 23 | .pnp.* 24 | .pnp/ 25 | 26 | # Private Files 27 | .env 28 | .idea 29 | .cake_task_cache 30 | 31 | # Build Caches 32 | build/ 33 | bower_components/ 34 | node_modules/ 35 | .next/ 36 | 37 | # ------------------------------------- 38 | # CDN Inclusions, Git Exclusions 39 | 40 | # Build Outputs 41 | **/out.* 42 | **/*.out.* 43 | **/out/ 44 | **/output/ 45 | *compiled* 46 | edition*/ 47 | coffeejs/ 48 | coffee/ 49 | es5/ 50 | es2015/ 51 | esnext/ 52 | docs/ 53 | 54 | # ===================================== 55 | # CUSTOM 56 | 57 | # None 58 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 2020 May 5 2 | # https://github.com/bevry/base 3 | 4 | # System Files 5 | **/.DS_Store 6 | 7 | # Temp Files 8 | **/.docpad.db 9 | **/*.log 10 | **/*.cpuprofile 11 | **/*.heapsnapshot 12 | 13 | # Editor Files 14 | .c9/ 15 | .vscode/ 16 | 17 | # Private Files 18 | .env 19 | .idea 20 | .cake_task_cache 21 | 22 | # Build Caches 23 | build/ 24 | components/ 25 | bower_components/ 26 | node_modules/ 27 | .pnp/ 28 | .pnp.js 29 | 30 | # Ecosystem Files 31 | .dependabout 32 | .github 33 | 34 | # ------------------------------------- 35 | # CDN Inclusions, Package Exclusions 36 | 37 | # Documentation Files 38 | docs/ 39 | guides/ 40 | BACKERS.md 41 | CONTRIBUTING.md 42 | HISTORY.md 43 | 44 | # Development Files 45 | web/ 46 | **/example* 47 | **/test* 48 | .babelrc* 49 | .editorconfig 50 | .eslintrc* 51 | .jshintrc 52 | .jscrc 53 | coffeelint* 54 | .travis* 55 | nakefile* 56 | Cakefile 57 | Makefile 58 | 59 | # Other Package Definitions 60 | template.js 61 | component.json 62 | bower.json 63 | 64 | # ===================================== 65 | # CUSTOM MODIFICATIONS 66 | 67 | # None 68 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Before You Post! 7 | 8 | ## Support 9 | 10 | We offer support through our [Official Support Channels](https://bevry.me/support). Do not use GitHub Issues for support, your issue will be closed. 11 | 12 | ## Contribute 13 | 14 | Our [Contributing Guide](https://bevry.me/contribute) contains useful tips and suggestions for how to contribute to this project, it's worth the read. 15 | 16 | ## Development 17 | 18 | ### Setup 19 | 20 | 1. [Install Node.js](https://bevry.me/install/node) 21 | 22 | 1. Fork the project and clone your fork - [guide](https://help.github.com/articles/fork-a-repo/) 23 | 24 | 1. Setup the project for development 25 | 26 | ```bash 27 | npm run our:setup 28 | ``` 29 | 30 | ### Developing 31 | 32 | 1. Compile changes 33 | 34 | ```bash 35 | npm run our:compile 36 | ``` 37 | 38 | 1. Run tests 39 | 40 | ```bash 41 | npm test 42 | ``` 43 | 44 | ### Publishing 45 | 46 | Follow these steps in order to implement your changes/improvements into your desired project: 47 | 48 | #### Preparation 49 | 50 | 1. Make sure your changes are on their own branch that is branched off from master. 51 | 52 | 1. You can do this by: `git checkout master; git checkout -b your-new-branch` 53 | 1. And push the changes up by: `git push origin your-new-branch` 54 | 55 | 1. Ensure all tests pass: 56 | 57 | ```bash 58 | npm test 59 | ``` 60 | 61 | > If possible, add tests for your change, if you don't know how, mention this in your pull request 62 | 63 | 1. Ensure the project is ready for publishing: 64 | 65 | ``` 66 | npm run our:release:prepare 67 | ``` 68 | 69 | #### Pull Request 70 | 71 | To send your changes for the project owner to merge in: 72 | 73 | 1. Submit your pull request 74 | 1. When submitting, if the original project has a `dev` or `integrate` branch, use that as the target branch for your pull request instead of the default `master` 75 | 1. By submitting a pull request you agree for your changes to have the same license as the original plugin 76 | 77 | #### Publish 78 | 79 | To publish your changes as the project owner: 80 | 81 | 1. Switch to the master branch: 82 | 83 | ```bash 84 | git checkout master 85 | ``` 86 | 87 | 1. Merge in the changes of the feature branch (if applicable) 88 | 89 | 1. Increment the version number in the `package.json` file according to the [semantic versioning](http://semver.org) standard, that is: 90 | 91 | 1. `x.0.0` MAJOR version when you make incompatible API changes (note: DocPad plugins must use v2 as the major version, as v2 corresponds to the current DocPad v6.x releases) 92 | 1. `x.y.0` MINOR version when you add functionality in a backwards-compatible manner 93 | 1. `x.y.z` PATCH version when you make backwards-compatible bug fixes 94 | 95 | 1. Add an entry to the changelog following the format of the previous entries, an example of this is: 96 | 97 | ```markdown 98 | ## v6.29.0 2013 April 1 99 | 100 | - Progress on [issue #474](https://github.com/docpad/docpad/issues/474) 101 | - DocPad will now set permissions based on the process's ability 102 | - Thanks to [Avi Deitcher](https://github.com/deitch), [Stephan Lough](https://github.com/stephanlough) for [issue #165](https://github.com/docpad/docpad/issues/165) 103 | - Updated dependencies 104 | ``` 105 | 106 | 1. Commit the changes with the commit title set to something like `v6.29.0. Bugfix. Improvement.` and commit description set to the changelog entry 107 | 108 | 1. Ensure the project is ready for publishing: 109 | 110 | ``` 111 | npm run our:release:prepare 112 | ``` 113 | 114 | 1. Prepare the release and publish it to npm and git: 115 | 116 | ```bash 117 | npm run our:release 118 | ``` 119 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## v6.11.0 2021 July 31 4 | 5 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 6 | 7 | ## v6.10.0 2021 July 28 8 | 9 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 10 | 11 | ## v6.9.0 2020 October 29 12 | 13 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 14 | 15 | ## v6.8.0 2020 September 5 16 | 17 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 18 | 19 | ## v6.7.0 2020 August 18 20 | 21 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 22 | 23 | ## v6.6.0 2020 August 4 24 | 25 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 26 | 27 | ## v6.5.0 2020 July 23 28 | 29 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 30 | 31 | ## v6.4.0 2020 June 25 32 | 33 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 34 | 35 | ## v6.3.0 2020 June 21 36 | 37 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 38 | 39 | ## v6.2.0 2020 June 10 40 | 41 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 42 | 43 | ## v6.1.0 2020 May 22 44 | 45 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 46 | 47 | ## v6.0.0 2020 May 21 48 | 49 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 50 | - Minimum required node version changed from `node: >=8` to `node: >=10` to keep up with mandatory ecosystem changes 51 | 52 | ## v5.6.0 2019 December 10 53 | 54 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 55 | 56 | ## v5.5.0 2019 December 1 57 | 58 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 59 | 60 | ## v5.4.0 2019 December 1 61 | 62 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 63 | 64 | ## v5.3.0 2019 November 18 65 | 66 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 67 | 68 | ## v5.2.0 2019 November 18 69 | 70 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 71 | 72 | ## v5.1.0 2019 November 13 73 | 74 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 75 | 76 | ## v5.0.0 2019 November 10 77 | 78 | - Updated dependencies, [base files](https://github.com/bevry/base) and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 79 | - Development dependency `rimraf` now requires node version `6` at minimum 80 | - As such, the minimum supported node version of watchr has changed from `0.12` to the latest LTS at the time of this release which is `8` 81 | 82 | ## v4.1.0 2018 December 7 83 | 84 | - Updated dependencies, [base files](https://github.com/bevry/base) and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 85 | 86 | ## v4.0.1 2018 January 24 87 | 88 | - Updated minimum supported node version to 0.12, due [readdir-cluster](https://github.com/bevry/readdir-cluster)'s use of Promises 89 | - Fixes [ReferenceError: Promise is not defined](https://travis-ci.org/bevry/watchr/jobs/332806957) 90 | 91 | ## v4.0.0 2018 January 24 92 | 93 | - This is an API backwards compatible release, however the underlying changes may introduce some problems, so a rolling adoption is warranted 94 | - Directory contents are now scanned in parallel 95 | - Directory scanning is now done by [scandirectory v3](https://github.com/bevry/scandirectory) (instead of v2) which uses [readdir-cluster](https://github.com/bevry/readdir-cluster) 96 | - readdir-cluster should offer performance benefits, but [currently does not run in non-master processes](https://github.com/bevry/readdir-cluster/issues/5) 97 | - Updated dependencies 98 | - Updated base files 99 | 100 | ## v3.0.1 2016 October 23 101 | 102 | - Fixed `open` not returning the stalker instance - Thanks to [Davide Mancuso](https://github.com/atomictag) for [issue #88](https://github.com/bevry/watchr/issues/88) 103 | - Fixed documentation on `create` not indicating that it returns the stalker instance 104 | - Updated base files 105 | 106 | ## v3.0.0 2016 October 19 107 | 108 | - Rewrote for better stability, all issues should now be considered closed 109 | - Converted from CoffeeScript to JavaScript 110 | - Node v0.8 support added once again (before node v0.12 was the earliest supported version) 111 | - Added jsdoc 112 | - Added flow type annotations 113 | 114 | ## v2.6.0 2016 July 15 115 | 116 | - Potentially fixed swapfiles breaking watching - Thanks to [Josh Levine](https://github.com/jlevine22) for [pull request #76](https://github.com/bevry/watchr/pull/76) 117 | 118 | ## v2.5.0 2016 July 15 119 | 120 | - Updated dependencies 121 | - Updated engines to be node >=0.12 as to align with safefs v4 - May still work with node 0.10, file a bug report if it doesn't 122 | 123 | ## v2.4.13 2015 February 7 124 | 125 | - Updated dependencies 126 | 127 | ## v2.4.12 2014 December 17 128 | 129 | - Fixed `previousStat` not existing sporadically on delete events - Thanks to [Stuart Knightley](https://github.com/Stuk) for [pull request #61](https://github.com/bevry/watchr/pull/61) 130 | - Updated dependencies 131 | 132 | ## v2.4.11 2014 February 7 133 | 134 | - Fixed interval option not beeing passed on to child watchers (regression since v2.4.7) - Thanks to [David Byrd](https://github.com/thebyrd) for [pull request #58](https://github.com/bevry/watchr/pull/58) 135 | 136 | ## v2.4.10 2014 February 7 137 | 138 | - Fixed watchr emitting error events incorrectly (regression since v2.4.7) - Thanks to [Aaron O'Mullan](https://github.com/AaronO) for [pull request #59](https://github.com/bevry/watchr/pull/59) 139 | 140 | ## v2.4.9 2014 January 28 141 | 142 | - Fixed `"me" is undefined` errors (regression since v2.4.7) 143 | 144 | ## v2.4.8 2013 December 30 145 | 146 | - You can now pass falsey values for`catchupDelay` to disable it 147 | 148 | ## v2.4.7 2013 December 19 149 | 150 | - Fixed: [Text Editor swap files on saving can throw it off](https://github.com/bevry/watchr/issues/33) 151 | - Fixed: [`ENOENT` errors are emitted when dead links a broken symlink is encountered](https://github.com/bevry/watchr/issues/42) 152 | - Updated dependencies 153 | 154 | ## v2.4.6 2013 November 18 155 | 156 | - Updated dependencies 157 | 158 | ## v2.4.5 2013 November 17 159 | 160 | - Updated dependencies 161 | 162 | ## v2.4.4 2013 October 10 163 | 164 | - Added the ability to turn off following links by setting `followLinks` to `false` - Thanks to [Fredrik Noren](https://github.com/FredrikNoren) for [pull request #47](https://github.com/bevry/watchr/pull/47) 165 | - Prefer accuracy over speed - Use the watch method by default, but don't trust it at all, always double check everything 166 | 167 | ## v2.4.3 2013 April 10 168 | 169 | - More work on swap file handling 170 | 171 | ## v2.4.2 2013 April 10 172 | 173 | - File copies will now trigger events throughout the copy rather than just at the start of the copy - Close [issue #35](https://github.com/bevry/watchr/issues/35) 174 | 175 | ## v2.4.1 2013 April 10 176 | 177 | - Fixed bubblr events 178 | - Fixed swap file detection 179 | 180 | ## v2.4.0 2013 April 5 181 | 182 | - Updated dependencies 183 | 184 | ## v2.3.10 2013 April 1 185 | 186 | - Updated dependencies 187 | 188 | ## v2.3.9 2013 March 17 189 | 190 | - Made it so if `duplicateDelay` is falsey we will not do a duplicate check 191 | 192 | ## v2.3.8 2013 March 17 193 | 194 | - Fix `Object # has no method 'emit'` error - Thanks to [Casey Foster](https://github.com/caseywebdev) for [pull request #32](https://github.com/bevry/watchr/pull/32) 195 | 196 | ## v2.3.7 2013 February 6 197 | 198 | - Changed the `preferredMethod` option into `preferredMethods` which accepts an array, defaults to `['watch','watchFile']` 199 | - If the watch action fails at the eve level we will try again with the preferredMethods reversed - This solves [issue #31](https://github.com/bevry/watchr/issues/31) where watching of large files would fail 200 | - Changed the `interval` option to default to `5007` (recommended by node) instead of `100` as it was before - The `watch` method provides us with immediate notification of changes without utilising polling, however the `watch` method fails for large amounts of files, in which case we will fall back to the `watchFile` method that will use this option, if the option is too small we will be constantly polling the large amount of files for changes using up all the CPU and memory, hence the change into a larger increment which has no CPU and memory impact. 201 | 202 | ## v2.3.6 2013 February 6 203 | 204 | - Fixed fallback when preferredMethod is `watchFile` 205 | 206 | ## v2.3.5 2013 February 6 207 | 208 | - Fixed uncaught exceptions when intialising watchers under certain circumstances 209 | 210 | ## v2.3.4 2013 February 5 211 | 212 | - Better handling and detection of failed watching operations 213 | - Better handling of duplicated events 214 | - Watching is now an atomic operation - If watching fails for a descendant, we will close everything related to that watch operation of the eve 215 | - We now prefer the `watch` method over the `watchFile` method - This offers great reliability and way less CPU and memory foot print - If you still wish to prefer `watchFile`, then set the new configuration option `preferredMethod` to `watchFile` 216 | - Closes [issue #30](https://github.com/bevry/watchr/issues/30) thanks to [Howard Tyson](https://github.com/tizzo) 217 | 218 | ## v2.3.3 2013 January 8 219 | 220 | - Added `outputLog` option 221 | - Added `ignorePaths` option - Thanks to [Tane Piper](https://github.com/tanepiper) for [issue #24](https://github.com/bevry/watchr/issues/24) 222 | - Now properly ignores hidden files - Thanks to [Ting-yu (Joseph) Chiang](https://github.com/josephj) for [issue #25](https://github.com/bevry/watchr/issues/25) and [Julien M.](https://github.com/julienma) for [issue #28](https://github.com/bevry/watchr/issues/28) 223 | - Added `Watcher::isIgnoredPath` method 224 | - Added tests for ignored and hidden files 225 | 226 | ## v2.3.2 2013 January 6 227 | 228 | - Fixed closing when a child path watcher doesn't exist - Closes [pull request #26](https://github.com/bevry/watchr/pull/26) thanks to [Jason Als](https://github.com/jasonals) 229 | - Added close tests 230 | 231 | ## v2.3.1 2012 December 19 232 | 233 | - Fixed a bug with closing directories that have children - Thanks to [Casey Foster](https://github.com/caseywebdev) for [issue #23](https://github.com/bevry/watchr/issues/23) 234 | 235 | ## v2.3.0 2012 December 17 236 | 237 | - This is a backwards compatiblity break, however updating is easy, read the notes below. 238 | - We've updated the events we emit to be: - `log` for debugging, receives the arguments `logLevel ,args...` - `watching` for when watching of the path has completed, receives the arguments `err, isWatching` - `change` for listening to change events, receives the arguments `changeType, fullPath, currentStat, previousStat` - `error` for gracefully listening to error events, receives the arguments `err` - read the README to learn how to bind to these new events 239 | - The `changeType` argument for change listeners has been changed for better clarity and consitency: - `change` is now `update` - `new` is now `create` - `unlink` is now `delete` 240 | - We've updated the return arguments for `require('watchr').watch` for better consitency: - if you send the `paths` option, you will receive the arguments `err, results` where `results` is an array of watcher instances - if you send the `path` option, you receive the arguments `err, watcherInstance` 241 | 242 | ## v2.2.1 2012 December 16 243 | 244 | - Fixed sub directory scans ignoring our ignore patterns 245 | - Updated dependencies 246 | 247 | ## v2.2.0 2012 December 15 248 | 249 | - We now ignore common ignore patterns by default 250 | - `ignorePatterns` configuration option renamed to `ignoreCommonPatterns` 251 | - Added new `ignoreCustomPatterns` configuration option 252 | - Updated dependencies - [bal-util](https://github.com/balupton/bal-util) from 1.13.x to 1.15.x 253 | - Closes [issue #22](https://github.com/bevry/watchr/issues/22) and [issue #21](https://github.com/bevry/watchr/issues/21) - Thanks [Andrew Petersen](https://github.com/kirbysayshi), [Sascha Depold](https://github.com/sdepold), [Raynos](https://github.com/Raynos), and [Prajwalit](https://github.com/prajwalit) for your help! 254 | 255 | ## v2.1.6 2012 November 6 256 | 257 | - Added missing `bin` configuration - Fixes [#16](https://github.com/bevry/watchr/issues/16) thanks to [pull request #17](https://github.com/bevry/watchr/pull/17) by [Robson Roberto Souza Peixoto](https://github.com/robsonpeixoto) 258 | 259 | ## v2.1.5 2012 September 29 260 | 261 | - Fixed completion callback not firing when trying to watch a path that doesn't exist 262 | 263 | ## v2.1.4 2012 September 27 264 | 265 | - Fixed new listeners not being added for directories that have already been watched 266 | - Fixed completion callbacks happening too soon 267 | - Thanks to [pull request #14](https://github.com/bevry/watchr/pull/14) by [Casey Foster](https://github.com/caseywebdev) 268 | 269 | ## v2.1.3 2012 August 10 270 | 271 | - Re-added markdown files to npm distribution as they are required for the npm website 272 | 273 | ## v2.1.2 2012 July 7 274 | 275 | - Fixed spelling of `persistent` 276 | - Explicitly set the defaults for the options `ignoreHiddenFiles` and `ignorePatterns` 277 | 278 | ## v2.1.1 2012 July 7 279 | 280 | - Added support for `interval` and `persistant` options 281 | - Improved unlink detection 282 | - Optimised unlink handling 283 | 284 | ## v2.1.0 2012 June 22 285 | 286 | - `watchr.watchr` changes - now only accepts one argument which is an object - added new `paths` property which is an array of multiple paths to watch - will only watch paths that actually exist (before it use to throw an error) 287 | - Fixed a few bugs 288 | - Added support for node v0.7/v0.8 289 | - Moved tests from Mocha to [Joe](https://github.com/bevry/joe) 290 | 291 | ## v2.0.3 2012 April 19 292 | 293 | - Fixed a bug with closing watchers 294 | - Now requires pre-compiled code 295 | 296 | ## v2.0.0 2012 April 19 297 | 298 | - Big rewrite 299 | - Got rid of the delay 300 | - Now always fires events 301 | - Watcher instsances inherit from Node's EventEmitter 302 | - Events for `change`, `unlink` and `new` 303 | 304 | ## v1.0.0 2012 February 11 305 | 306 | - Better support for ignoring hidden files 307 | - Improved documentation, readme 308 | - Added `History.md` file 309 | - Added unit tests using [Mocha](http://visionmedia.github.com/mocha/) 310 | 311 | ## v0.1.0 2012 November 13 312 | 313 | - Initial working version 314 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

License

4 | 5 | Unless stated otherwise all works are: 6 | 7 | 9 | 10 | and licensed under: 11 | 12 | 13 | 14 |

MIT License

15 | 16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
18 | 
19 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
20 | 
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | 
23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

watchr

4 | 5 | 6 | 7 | 8 | 9 | 10 | Status of the GitHub Workflow: bevry 11 | NPM version 12 | NPM downloads 13 | Dependency Status 14 | Dev Dependency Status 15 |
16 | GitHub Sponsors donate button 17 | Patreon donate button 18 | Flattr donate button 19 | Liberapay donate button 20 | Buy Me A Coffee donate button 21 | Open Collective donate button 22 | crypto donate button 23 | PayPal donate button 24 | Wishlist browse button 25 | 26 | 27 | 28 | 29 | Watchr provides a normalised API the file watching APIs of different node versions, nested/recursive file and directory watching, and accurate detailed events for file/directory creations, updates, and deletions. 30 | 31 | ## Usage 32 | 33 | [Complete API Documentation.](http://master.watchr.bevry.surge.sh/docs/) 34 | 35 | There are two concepts in watchr, they are: 36 | 37 | - Watcher - this wraps the native file system watching, makes it reliable, and supports deep watching 38 | - Stalker - this wraps the watcher, such that for any given path, there can be many stalkers, but only one watcher 39 | 40 | The simplest usage is: 41 | 42 | ```javascript 43 | // Import the watching library 44 | var watchr = require('watchr') 45 | 46 | // Define our watching parameters 47 | var path = process.cwd() 48 | function listener(changeType, fullPath, currentStat, previousStat) { 49 | switch (changeType) { 50 | case 'update': 51 | console.log( 52 | 'the file', 53 | fullPath, 54 | 'was updated', 55 | currentStat, 56 | previousStat 57 | ) 58 | break 59 | case 'create': 60 | console.log('the file', fullPath, 'was created', currentStat) 61 | break 62 | case 'delete': 63 | console.log('the file', fullPath, 'was deleted', previousStat) 64 | break 65 | } 66 | } 67 | function next(err) { 68 | if (err) return console.log('watch failed on', path, 'with error', err) 69 | console.log('watch successful on', path) 70 | } 71 | 72 | // Watch the path with the change listener and completion callback 73 | var stalker = watchr.open(path, listener, next) 74 | 75 | // Close the stalker of the watcher 76 | stalker.close() 77 | ``` 78 | 79 | More advanced usage is: 80 | 81 | ```javascript 82 | // Create the stalker for the path 83 | var stalker = watchr.create(path) 84 | 85 | // Listen to the events for the stalker/watcher 86 | stalker.on('change', listener) 87 | stalker.on('log', console.log) 88 | stalker.once('close', function (reason) { 89 | console.log('closed', path, 'because', reason) 90 | stalker.removeAllListeners() // as it is closed, no need for our change or log listeners any more 91 | }) 92 | 93 | // Set the default configuration for the stalker/watcher 94 | stalker.setConfig({ 95 | stat: null, 96 | interval: 5007, 97 | persistent: true, 98 | catchupDelay: 2000, 99 | preferredMethods: ['watch', 'watchFile'], 100 | followLinks: true, 101 | ignorePaths: false, 102 | ignoreHiddenFiles: false, 103 | ignoreCommonPatterns: true, 104 | ignoreCustomPatterns: null, 105 | }) 106 | 107 | // Start watching 108 | stalker.watch(next) 109 | 110 | // Stop watching 111 | stalker.close() 112 | ``` 113 | 114 | 115 | 116 |

Install

117 | 118 |

npm

119 | 124 | 125 |

Editions

126 | 127 |

This package is published with the following editions:

128 | 129 | 131 | 132 |

TypeScript

133 | 134 | This project provides its type information via inline JSDoc Comments. To make use of this in TypeScript, set your maxNodeModuleJsDepth compiler option to `5` or thereabouts. You can accomlish this via your `tsconfig.json` file like so: 135 | 136 | ``` json 137 | { 138 | "compilerOptions": { 139 | "maxNodeModuleJsDepth": 5 140 | } 141 | } 142 | ``` 143 | 144 | 145 | 146 | 147 | 148 | 149 |

History

150 | 151 | Discover the release history by heading on over to the HISTORY.md file. 152 | 153 | 154 | 155 | 156 | 157 | 158 |

Contribute

159 | 160 | Discover how you can contribute by heading on over to the CONTRIBUTING.md file. 161 | 162 | 163 | 164 | 165 | 166 | 167 |

Backers

168 | 169 |

Maintainers

170 | 171 | These amazing people are maintaining this project: 172 | 173 | 174 | 175 |

Sponsors

176 | 177 | No sponsors yet! Will you be the first? 178 | 179 | GitHub Sponsors donate button 180 | Patreon donate button 181 | Flattr donate button 182 | Liberapay donate button 183 | Buy Me A Coffee donate button 184 | Open Collective donate button 185 | crypto donate button 186 | PayPal donate button 187 | Wishlist browse button 188 | 189 |

Contributors

190 | 191 | These amazing people have contributed code to this project: 192 | 193 | 202 | 203 | Discover how you can contribute by heading on over to the CONTRIBUTING.md file. 204 | 205 | 206 | 207 | 208 | 209 | 210 |

License

211 | 212 | Unless stated otherwise all works are: 213 | 214 | 216 | 217 | and licensed under: 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watchr", 3 | "version": "6.11.0", 4 | "description": "Better file system watching for Node.js", 5 | "homepage": "https://github.com/bevry/watchr", 6 | "license": "MIT", 7 | "keywords": [ 8 | "esnext", 9 | "fs", 10 | "fswatcher", 11 | "node", 12 | "typed", 13 | "types", 14 | "watch", 15 | "watchfile", 16 | "watching" 17 | ], 18 | "badges": { 19 | "list": [ 20 | "githubworkflow", 21 | "npmversion", 22 | "npmdownloads", 23 | "daviddm", 24 | "daviddmdev", 25 | "---", 26 | "githubsponsors", 27 | "patreon", 28 | "flattr", 29 | "liberapay", 30 | "buymeacoffee", 31 | "opencollective", 32 | "crypto", 33 | "paypal", 34 | "wishlist" 35 | ], 36 | "config": { 37 | "githubWorkflow": "bevry", 38 | "githubSponsorsUsername": "balupton", 39 | "buymeacoffeeUsername": "balupton", 40 | "cryptoURL": "https://bevry.me/crypto", 41 | "flattrUsername": "balupton", 42 | "liberapayUsername": "bevry", 43 | "opencollectiveUsername": "bevry", 44 | "patreonUsername": "bevry", 45 | "paypalURL": "https://bevry.me/paypal", 46 | "wishlistURL": "https://bevry.me/wishlist", 47 | "githubUsername": "bevry", 48 | "githubRepository": "watchr", 49 | "githubSlug": "bevry/watchr", 50 | "npmPackageName": "watchr" 51 | } 52 | }, 53 | "funding": "https://bevry.me/fund", 54 | "author": "2012+ Bevry Pty Ltd (http://bevry.me), 2011 Benjamin Lupton (https://balupton.com)", 55 | "maintainers": [ 56 | "Benjamin Lupton (https://github.com/balupton)" 57 | ], 58 | "contributors": [ 59 | "Aaron O'Mullan (https://github.com/AaronO)", 60 | "Adam Sanderson (https://github.com/adamsanderson)", 61 | "Benjamin Lupton (https://github.com/balupton)", 62 | "Casey Foster (https://github.com/caseywebdev)", 63 | "David Byrd (http://digitalocean.com)", 64 | "Fredrik Norén (https://github.com/FredrikNoren)", 65 | "Josh Levine (https://github.com/jlevine22)", 66 | "Robson Roberto Souza Peixoto (https://github.com/robsonpeixoto)", 67 | "Stuart Knightley (https://github.com/Stuk)" 68 | ], 69 | "bugs": { 70 | "url": "https://github.com/bevry/watchr/issues" 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "https://github.com/bevry/watchr.git" 75 | }, 76 | "engines": { 77 | "node": ">=10" 78 | }, 79 | "editions": [ 80 | { 81 | "description": "ESNext source code for Node.js 10 || 12 || 14 || 16 with Require for modules", 82 | "directory": "source", 83 | "entry": "index.js", 84 | "tags": [ 85 | "source", 86 | "javascript", 87 | "esnext", 88 | "require" 89 | ], 90 | "engines": { 91 | "node": "10 || 12 || 14 || 16" 92 | } 93 | } 94 | ], 95 | "type": "commonjs", 96 | "main": "source/index.js", 97 | "dependencies": { 98 | "eachr": "^4.5.0", 99 | "extendr": "^5.19.0", 100 | "ignorefs": "^3.17.0", 101 | "safefs": "^6.16.0", 102 | "scandirectory": "^6.16.0", 103 | "taskgroup": "^7.19.0" 104 | }, 105 | "devDependencies": { 106 | "@bevry/update-contributors": "^1.20.0", 107 | "assert-helpers": "^8.4.0", 108 | "bal-util": "^2.8.0", 109 | "core-js": "^3.16.0", 110 | "eslint": "^7.31.0", 111 | "eslint-config-bevry": "^3.27.0", 112 | "eslint-config-prettier": "^8.3.0", 113 | "eslint-plugin-prettier": "^3.4.0", 114 | "jsdoc": "^3.6.7", 115 | "kava": "^5.15.0", 116 | "prettier": "^2.3.2", 117 | "projectz": "^2.22.0", 118 | "rimraf": "^3.0.2", 119 | "surge": "^0.23.0", 120 | "valid-directory": "^3.9.0" 121 | }, 122 | "scripts": { 123 | "our:clean": "rm -Rf ./docs ./edition* ./es2015 ./es5 ./out ./.next", 124 | "our:compile": "echo no need for this project", 125 | "our:deploy": "echo no need for this project", 126 | "our:meta": "npm run our:meta:contributors && npm run our:meta:docs && npm run our:meta:projectz", 127 | "our:meta:contributors": "update-contributors", 128 | "our:meta:docs": "npm run our:meta:docs:jsdoc", 129 | "our:meta:docs:jsdoc": "rm -Rf ./docs && jsdoc --recurse --pedantic --access all --destination ./docs --package ./package.json --readme ./README.md ./source && mv ./docs/$npm_package_name/$npm_package_version/* ./docs/ && rm -Rf ./docs/$npm_package_name/$npm_package_version", 130 | "our:meta:projectz": "projectz compile", 131 | "our:release": "npm run our:release:prepare && npm run our:release:check-changelog && npm run our:release:check-dirty && npm run our:release:tag && npm run our:release:push", 132 | "our:release:check-changelog": "cat ./HISTORY.md | grep v$npm_package_version || (echo add a changelog entry for v$npm_package_version && exit -1)", 133 | "our:release:check-dirty": "git diff --exit-code", 134 | "our:release:prepare": "npm run our:clean && npm run our:compile && npm run our:test && npm run our:meta", 135 | "our:release:push": "git push origin && git push origin --tags", 136 | "our:release:tag": "export MESSAGE=$(cat ./HISTORY.md | sed -n \"/## v$npm_package_version/,/##/p\" | sed 's/## //' | awk 'NR>1{print buf}{buf = $0}') && test \"$MESSAGE\" || (echo 'proper changelog entry not found' && exit -1) && git tag v$npm_package_version -am \"$MESSAGE\"", 137 | "our:setup": "npm run our:setup:install", 138 | "our:setup:install": "npm install", 139 | "our:test": "npm run our:verify && npm test", 140 | "our:verify": "npm run our:verify:directory && npm run our:verify:eslint && npm run our:verify:prettier", 141 | "our:verify:directory": "valid-directory", 142 | "our:verify:eslint": "eslint --fix --ignore-pattern '**/*.d.ts' --ignore-pattern '**/vendor/' --ignore-pattern '**/node_modules/' --ext .mjs,.js,.jsx,.ts,.tsx ./source", 143 | "our:verify:prettier": "prettier --write .", 144 | "test": "node ./source/test.js" 145 | }, 146 | "eslintConfig": { 147 | "extends": [ 148 | "bevry" 149 | ] 150 | }, 151 | "prettier": { 152 | "semi": false, 153 | "singleQuote": true 154 | } 155 | } -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint no-use-before-define:0 */ 3 | 'use strict' 4 | 5 | // Imports 6 | const pathUtil = require('path') 7 | const scandir = require('scandirectory') 8 | const fsUtil = require('safefs') 9 | const ignorefs = require('ignorefs') 10 | const extendr = require('extendr') 11 | const eachr = require('eachr') 12 | const { TaskGroup } = require('taskgroup') 13 | const { EventEmitter } = require('events') 14 | 15 | /* :: 16 | import type {Stats, FSWatcher} from 'fs' 17 | type StateEnum = "pending" | "active" | "deleted" | "closed" 18 | type MethodEnum = "watch" | "watchFile" 19 | type ErrorCallback = (error: ?Error) => void 20 | type StatCallback = (error: ?Error, stat?: Stats) => void 21 | type WatchChildOpts = { 22 | fullPath: string, 23 | relativePath: string, 24 | stat?: Stats 25 | } 26 | type WatchSelfOpts = { 27 | errors?: Array, 28 | preferredMethods?: Array 29 | } 30 | type ListenerOpts = { 31 | method: MethodEnum, 32 | args: Array 33 | } 34 | type ResetOpts = { 35 | reset?: boolean 36 | } 37 | type IgnoreOpts = { 38 | ignorePaths?: boolean, 39 | ignoreHiddenFiles?: boolean, 40 | ignoreCommonPatterns?: boolean, 41 | ignoreCustomPatterns?: RegExp 42 | } 43 | type WatcherOpts = IgnoreOpts & { 44 | stat?: Stats, 45 | interval?: number, 46 | persistent?: boolean, 47 | catchupDelay?: number, 48 | preferredMethods?: Array, 49 | followLinks?: boolean 50 | } 51 | type WatcherConfig = { 52 | stat: ?Stats, 53 | interval: number, 54 | persistent: boolean, 55 | catchupDelay: number, 56 | preferredMethods: Array, 57 | followLinks: boolean, 58 | ignorePaths: false | Array, 59 | ignoreHiddenFiles: boolean, 60 | ignoreCommonPatterns: boolean, 61 | ignoreCustomPatterns: ?RegExp 62 | } 63 | */ 64 | 65 | // Helper for error logging 66 | function errorToString(error /* :Error */) { 67 | return error.stack.toString() || error.message || error.toString() 68 | } 69 | 70 | /** 71 | Alias for creating a new {@link Stalker} with some basic configuration 72 | @access public 73 | @param {string} path - the path to watch 74 | @param {function} changeListener - the change listener for {@link Watcher} 75 | @param {function} next - the completion callback for {@link Watcher#watch} 76 | @returns {Stalker} 77 | */ 78 | function open( 79 | path /* :string */, 80 | changeListener /* :function */, 81 | next /* :function */ 82 | ) { 83 | const stalker = new Stalker(path) 84 | stalker.on('change', changeListener) 85 | stalker.watch({}, next) 86 | return stalker 87 | } 88 | 89 | /** 90 | Alias for creating a new {@link Stalker} 91 | @access public 92 | @returns {Stalker} 93 | */ 94 | function create(...args /* :Array */) { 95 | return new Stalker(...args) 96 | } 97 | 98 | /** 99 | Stalker 100 | A watcher of the watchers. 101 | Events that are listened to on the stalker will also be listened to on the attached watcher. 102 | When the watcher is closed, the stalker's listeners will be removed. 103 | When all stalkers for a watcher are removed, the watcher will close. 104 | @protected 105 | @property {Object} watchers - static collection of all the watchers mapped by path 106 | @property {Watcher} watcher - the associated watcher for this stalker 107 | */ 108 | class Stalker extends EventEmitter { 109 | /* :: static watchers: {[key:string]: Watcher}; */ 110 | /* :: watcher: Watcher; */ 111 | 112 | /** 113 | @param {string} path - the path to watch 114 | */ 115 | constructor(path /* :string */) { 116 | super() 117 | 118 | // Ensure global watchers singleton 119 | if (Stalker.watchers == null) Stalker.watchers = {} 120 | 121 | // Add our watcher to the singleton 122 | if (Stalker.watchers[path] == null) 123 | Stalker.watchers[path] = new Watcher(path) 124 | this.watcher = Stalker.watchers[path] 125 | 126 | // Add our stalker to the watcher 127 | if (this.watcher.stalkers == null) this.watcher.stalkers = [] 128 | this.watcher.stalkers.push(this) 129 | 130 | // If the watcher closes, remove our stalker and the watcher from the singleton 131 | this.watcher.once('close', () => { 132 | this.remove() 133 | delete Stalker.watchers[path] 134 | }) 135 | 136 | // Add the listener proxies 137 | this.on('newListener', (eventName, listener) => 138 | this.watcher.on(eventName, listener) 139 | ) 140 | this.on('removeListener', (eventName, listener) => 141 | this.watcher.removeListener(eventName, listener) 142 | ) 143 | } 144 | 145 | /** 146 | Cleanly shutdown the stalker 147 | @private 148 | @returns {this} 149 | */ 150 | remove() { 151 | // Remove our stalker from the watcher 152 | const index = this.watcher.stalkers.indexOf(this) 153 | if (index !== -1) { 154 | this.watcher.stalkers = this.watcher.stalkers 155 | .slice(0, index) 156 | .concat(this.watcher.stalkers.slice(index + 1)) 157 | } 158 | 159 | // Kill our stalker 160 | process.nextTick(() => { 161 | this.removeAllListeners() 162 | }) 163 | 164 | // Chain 165 | return this 166 | } 167 | 168 | /** 169 | Close the stalker, and if it is the last stalker for the path, close the watcher too 170 | @access public 171 | @param {string} [reason] - optional reason to provide for closure 172 | @returns {this} 173 | */ 174 | close(reason /* :?string */) { 175 | // Remove our stalker 176 | this.remove() 177 | 178 | // If it was the last stalker for the watcher, or if the path is deleted 179 | // Then close the watcher 180 | if (reason === 'deleted' || this.watcher.stalkers.length === 0) { 181 | this.watcher.close(reason || 'all stalkers are now gone') 182 | } 183 | 184 | // Chain 185 | return this 186 | } 187 | 188 | /** 189 | Alias for {@link Watcher#setConfig} 190 | @access public 191 | @returns {this} 192 | */ 193 | setConfig(...args /* :Array */) { 194 | this.watcher.setConfig(...args) 195 | return this 196 | } 197 | 198 | /** 199 | Alias for {@link Watcher#watch} 200 | @access public 201 | @returns {this} 202 | */ 203 | watch(...args /* :Array */) { 204 | this.watcher.watch(...args) 205 | return this 206 | } 207 | } 208 | 209 | /** 210 | Watcher 211 | Watches a path and if its a directory, its children too, and emits change events for updates, deletions, and creations 212 | 213 | Available events: 214 | 215 | - `log(logLevel, ...args)` - emitted for debugging, child events are bubbled up 216 | - `close(reason)` - the watcher has been closed, perhaps for a reason 217 | - `change('update', fullPath, currentStat, previousStat)` - an update event has occured on the `fullPath` 218 | - `change('delete', fullPath, currentStat)` - an delete event has occured on the `fullPath` 219 | - `change('create', fullPath, null, previousStat)` - a create event has occured on the `fullPath` 220 | 221 | @protected 222 | @property {Array} stalkers - the associated stalkers for this watcher 223 | @property {string} path - the path to be watched 224 | @property {Stats} stat - the stat object for the path 225 | @property {FSWatcher} fswatcher - if the `watch` method was used, this is the FSWatcher instance for it 226 | @property {Object} children - a (relativePath => stalker) mapping of children 227 | @property {string} state - the current state of this watcher 228 | @property {TaskGroup} listenerTaskGroup - the TaskGroup instance for queuing listen events 229 | @property {TimeoutID} listenerTimeout - the timeout result for queuing listen events 230 | @property {Object} config - the configuration options 231 | */ 232 | class Watcher extends EventEmitter { 233 | /* :: stalkers: Array; */ 234 | 235 | /* :: path: string; */ 236 | /* :: stat: null | Stats; */ 237 | /* :: fswatcher: null | FSWatcher; */ 238 | /* :: children: {[path:string]: Stalker}; */ 239 | /* :: state: StateEnum; */ 240 | /* :: listenerTaskGroup: null | TaskGroup; */ 241 | /* :: listenerTimeout: null | TimeoutID; */ 242 | /* :: config: WatcherConfig; */ 243 | 244 | /** 245 | @param {string} path - the path to watch 246 | */ 247 | constructor(path /* :string */) { 248 | // Construct the EventEmitter 249 | super() 250 | 251 | // Initialise properties 252 | this.path = path 253 | this.stat = null 254 | this.fswatcher = null 255 | this.children = {} 256 | this.state = 'pending' 257 | this.listenerTaskGroup = null 258 | this.listenerTimeout = null 259 | 260 | // Initialize our configurable properties 261 | this.config = { 262 | stat: null, 263 | interval: 5007, 264 | persistent: true, 265 | catchupDelay: 2000, 266 | preferredMethods: ['watch', 'watchFile'], 267 | followLinks: true, 268 | ignorePaths: false, 269 | ignoreHiddenFiles: false, 270 | ignoreCommonPatterns: true, 271 | ignoreCustomPatterns: null, 272 | } 273 | } 274 | 275 | /** 276 | Configure out Watcher 277 | @param {Object} opts - the configuration to use 278 | @param {Stats} [opts.stat] - A stat object for the path if we already have one, otherwise it will be fetched for us 279 | @param {number} [opts.interval=5007] - If the `watchFile` method was used, this is the interval to use for change detection if polling is needed 280 | @param {boolean} [opts.persistent=true] - If the `watchFile` method was used, this is whether or not watching should keep the node process alive while active 281 | @param {number} [opts.catchupDelay=2000] - This is the delay to wait after a change event to be able to detect swap file changes accurately (without a delay, swap files trigger a delete and creation event, with a delay they trigger a single update event) 282 | @param {Array} [opts.preferredMethods=['watch', 'watchFile']] - The order of watch methods to attempt, if the first fails, move onto the second 283 | @param {boolean} [opts.followLinks=true] - If true, will use `fs.stat` instead of `fs.lstat` 284 | @param {Array} [opts.ignorePaths=false] - Array of paths that should be ignored 285 | @param {boolean} [opts.ignoreHiddenFiles=false] - Whether to ignore files and directories that begin with a `.` 286 | @param {boolean} [opts.ignoreCommonPatterns=false] - Whether to ignore common undesirable paths (e.g. `.svn`, `.git`, `.DS_Store`, `thumbs.db`, etc) 287 | @param {RegExp} [opts.ignoreCustomPatterns] - A regular expression that if matched again the path will ignore the path 288 | @returns {this} 289 | */ 290 | setConfig(opts /* :WatcherOpts */) { 291 | // Apply 292 | extendr.extend(this.config, opts) 293 | 294 | // Stat 295 | if (this.config.stat) { 296 | this.stat = this.config.stat 297 | delete this.config.stat 298 | } 299 | 300 | // Chain 301 | return this 302 | } 303 | 304 | /** 305 | Emit a log event with the given arguments 306 | @param {Array<*>} args 307 | @returns {this} 308 | */ 309 | log(...args /* :Array */) { 310 | // Emit the log event 311 | this.emit('log', ...args) 312 | 313 | // Chain 314 | return this 315 | } 316 | 317 | /** 318 | Fetch the ignored configuration options into their own object 319 | @private 320 | @returns {Object} 321 | */ 322 | getIgnoredOptions() { 323 | // Return the ignore options 324 | return { 325 | ignorePaths: this.config.ignorePaths, 326 | ignoreHiddenFiles: this.config.ignoreHiddenFiles, 327 | ignoreCommonPatterns: this.config.ignoreCommonPatterns, 328 | ignoreCustomPatterns: this.config.ignoreCustomPatterns, 329 | } 330 | } 331 | 332 | /** 333 | Check whether or not a path should be ignored or not based on our current configuration options 334 | @private 335 | @param {String} path - the path (likely of a child) 336 | @returns {boolean} 337 | */ 338 | isIgnoredPath(path /* :string */) { 339 | // Ignore? 340 | const ignore = ignorefs.isIgnoredPath(path, this.getIgnoredOptions()) 341 | 342 | // Return 343 | return ignore 344 | } 345 | 346 | /** 347 | Get the stat for the path of the watcher 348 | If the stat already exists and `opts.reset` is `false`, then just use the current stat, otherwise fetch a new stat and apply it to the watcher 349 | @param {Object} opts 350 | @param {boolean} [opts.reset=false] 351 | @param {function} next - completion callback with signature `error:?Error, stat?:Stats` 352 | @returns {this} 353 | */ 354 | getStat(opts /* :ResetOpts */, next /* :StatCallback */) { 355 | // Figure out what stat method we want to use 356 | const method = this.config.followLinks ? 'stat' : 'lstat' 357 | 358 | // Fetch 359 | if (this.stat && opts.reset !== true) { 360 | next(null, this.stat) 361 | } else { 362 | fsUtil[method](this.path, (err, stat) => { 363 | if (err) return next(err) 364 | this.stat = stat 365 | return next(null, stat) 366 | }) 367 | } 368 | 369 | // Chain 370 | return this 371 | } 372 | 373 | /** 374 | Watch and WatchFile Listener 375 | The listener attached to the `watch` and `watchFile` watching methods. 376 | 377 | Things to note: 378 | - `watchFile` method: 379 | - Arguments: 380 | - currentStat - the updated stat of the changed file 381 | - Exists even for deleted/renamed files 382 | - previousStat - the last old stat of the changed file 383 | - Is accurate, however we already have this 384 | - For renamed files, it will will fire on the directory and the file 385 | - `watch` method: 386 | - Arguments: 387 | - eventName - either 'rename' or 'change' 388 | - THIS VALUE IS ALWAYS UNRELIABLE AND CANNOT BE TRUSTED 389 | - filename - child path of the file that was triggered 390 | - This value can also be unrealiable at times 391 | - both methods: 392 | - For deleted and changed files, it will fire on the file 393 | - For new files, it will fire on the directory 394 | 395 | Output arguments for your emitted event will be: 396 | - for updated files the arguments will be: `'update', fullPath, currentStat, previousStat` 397 | - for created files the arguments will be: `'create', fullPath, currentStat, null` 398 | - for deleted files the arguments will be: `'delete', fullPath, null, previousStat` 399 | 400 | In the future we will add: 401 | - for renamed files: 'rename', fullPath, currentStat, previousStat, newFullPath 402 | - rename is possible as the stat.ino is the same for the delete and create 403 | 404 | @private 405 | @param {Object} opts 406 | @param {string} [opts.method] - the watch method that was used 407 | @param {Array<*>} [opts.args] - the arguments from the watching method 408 | @param {function} [next] - the optional completion callback with the signature `(error:?Error)` 409 | @returns {this} 410 | */ 411 | listener(opts /* :ListenerOpts */, next /* ::?:ErrorCallback */) { 412 | // Prepare 413 | const config = this.config 414 | const method = opts.method 415 | if (!next) { 416 | next = (err) => { 417 | if (err) { 418 | this.emit('error', err) 419 | } 420 | } 421 | } 422 | 423 | // Prepare properties 424 | let currentStat = null 425 | let previousStat = null 426 | 427 | // Log 428 | this.log('debug', `watch via ${method} method fired on: ${this.path}`) 429 | 430 | // Delay the execution of the listener tasks, to once the change events have stopped firing 431 | if (this.listenerTimeout != null) { 432 | clearTimeout(this.listenerTimeout) 433 | } 434 | this.listenerTimeout = setTimeout(() => { 435 | const tasks = this.listenerTaskGroup 436 | if (tasks) { 437 | this.listenerTaskGroup = null 438 | this.listenerTimeout = null 439 | tasks.run() 440 | } else { 441 | this.emit('error', new Error('unexpected state')) 442 | } 443 | }, config.catchupDelay || 0) 444 | 445 | // We are a subsequent listener, in which case, just listen to the first listener tasks 446 | if (this.listenerTaskGroup != null) { 447 | this.listenerTaskGroup.done(next) 448 | return this 449 | } 450 | 451 | // Start the detection process 452 | const tasks = (this.listenerTaskGroup = new TaskGroup( 453 | `listener tasks for ${this.path}`, 454 | { domain: false } 455 | ).done(next)) 456 | tasks.addTask('check if the file still exists', (complete) => { 457 | // Log 458 | this.log( 459 | 'debug', 460 | `watch evaluating on: ${this.path} [state: ${this.state}]` 461 | ) 462 | 463 | // Check if this is still needed 464 | if (this.state !== 'active') { 465 | this.log('debug', `watch discarded on: ${this.path}`) 466 | tasks.clearRemaining() 467 | return complete() 468 | } 469 | 470 | // Check if the file still exists 471 | fsUtil.exists(this.path, (exists) => { 472 | // Apply local global property 473 | previousStat = this.stat 474 | 475 | // If the file still exists, then update the stat 476 | if (exists === false) { 477 | // Log 478 | this.log('debug', `watch emit delete: ${this.path}`) 479 | 480 | // Apply 481 | this.stat = null 482 | this.close('deleted') 483 | this.emit('change', 'delete', this.path, null, previousStat) 484 | 485 | // Clear the remaining tasks, as they are no longer needed 486 | tasks.clearRemaining() 487 | return complete() 488 | } 489 | 490 | // Update the stat of the fil 491 | this.getStat({ reset: true }, (err, stat) => { 492 | // Check 493 | if (err) return complete(err) 494 | 495 | // Update 496 | currentStat = stat 497 | 498 | // Complete 499 | return complete() 500 | }) 501 | }) 502 | }) 503 | 504 | tasks.addTask('check if the file has changed', (complete) => { 505 | // Ensure stats exist 506 | if (!currentStat || !previousStat) { 507 | return complete(new Error('unexpected state')) 508 | } 509 | 510 | // Check if there is a different file at the same location 511 | // If so, we will need to rewatch the location and the children 512 | if (currentStat.ino.toString() !== previousStat.ino.toString()) { 513 | this.log( 514 | 'debug', 515 | `watch found replaced: ${this.path}`, 516 | currentStat, 517 | previousStat 518 | ) 519 | // note this will close the entire tree of listeners and reinstate them 520 | // however, as this is probably for a file, it is probably not that bad 521 | return this.watch({ reset: true }, complete) 522 | } 523 | 524 | // Check if the file or directory has been modified 525 | if (currentStat.mtime.toString() !== previousStat.mtime.toString()) { 526 | this.log( 527 | 'debug', 528 | `watch found modification: ${this.path}`, 529 | previousStat, 530 | currentStat 531 | ) 532 | return complete() 533 | } 534 | 535 | // Otherwise it is the same, and nothing is needed to be done 536 | else { 537 | tasks.clearRemaining() 538 | return complete() 539 | } 540 | }) 541 | 542 | tasks.addGroup('check what has changed', (addGroup, addTask, done) => { 543 | // Ensure stats exist 544 | if (!currentStat || !previousStat) { 545 | return done(new Error('unexpected state')) 546 | } 547 | 548 | // Set this sub group to execute in parallel 549 | this.setConfig({ concurrency: 0 }) 550 | 551 | // So let's check if we are a directory 552 | if (currentStat.isDirectory() === false) { 553 | // If we are a file, lets simply emit the change event 554 | this.log('debug', `watch emit update: ${this.path}`) 555 | this.emit('change', 'update', this.path, currentStat, previousStat) 556 | return done() 557 | } 558 | 559 | // We are a direcotry 560 | // Chances are something actually happened to a child (rename or delete) 561 | // and if we are the same, then we should scan our children to look for renames and deletes 562 | fsUtil.readdir(this.path, (err, newFileRelativePaths) => { 563 | // Error? 564 | if (err) return done(err) 565 | 566 | // Log 567 | this.log('debug', `watch read dir: ${this.path}`, newFileRelativePaths) 568 | 569 | // Find deleted files 570 | eachr(this.children, (child, childFileRelativePath) => { 571 | // Skip if the file still exists 572 | if (newFileRelativePaths.indexOf(childFileRelativePath) !== -1) return 573 | 574 | // Fetch full path 575 | const childFileFullPath = pathUtil.join( 576 | this.path, 577 | childFileRelativePath 578 | ) 579 | 580 | // Skip if ignored file 581 | if (this.isIgnoredPath(childFileFullPath)) { 582 | this.log( 583 | 'debug', 584 | `watch ignored delete: ${childFileFullPath} via: ${this.path}` 585 | ) 586 | return 587 | } 588 | 589 | // Emit the event and note the change 590 | this.log( 591 | 'debug', 592 | `watch emit delete: ${childFileFullPath} via: ${this.path}` 593 | ) 594 | const childPreviousStat = child.watcher.stat 595 | child.close('deleted') 596 | this.emit( 597 | 'change', 598 | 'delete', 599 | childFileFullPath, 600 | null, 601 | childPreviousStat 602 | ) 603 | }) 604 | 605 | // Find new files 606 | eachr(newFileRelativePaths, (childFileRelativePath) => { 607 | // Skip if we are already watching this file 608 | if (this.children[childFileRelativePath] != null) return 609 | 610 | // Fetch full path 611 | const childFileFullPath = pathUtil.join( 612 | this.path, 613 | childFileRelativePath 614 | ) 615 | 616 | // Skip if ignored file 617 | if (this.isIgnoredPath(childFileFullPath)) { 618 | this.log( 619 | 'debug', 620 | `watch ignored create: ${childFileFullPath} via: ${this.path}` 621 | ) 622 | return 623 | } 624 | 625 | // Emit the event and note the change 626 | addTask('watch the new child', (complete) => { 627 | this.log( 628 | 'debug', 629 | `watch determined create: ${childFileFullPath} via: ${this.path}` 630 | ) 631 | if (this.children[childFileRelativePath] != null) { 632 | return complete() // this should never occur 633 | } 634 | const child = this.watchChild( 635 | { 636 | fullPath: childFileFullPath, 637 | relativePath: childFileRelativePath, 638 | }, 639 | (err) => { 640 | if (err) return complete(err) 641 | this.emit( 642 | 'change', 643 | 'create', 644 | childFileFullPath, 645 | child.watcher.stat, 646 | null 647 | ) 648 | return complete() 649 | } 650 | ) 651 | }) 652 | }) 653 | 654 | // Read the directory, finished adding tasks to the group 655 | return done() 656 | }) 657 | }) 658 | 659 | // Tasks are executed via the timeout thing earlier 660 | 661 | // Chain 662 | return this 663 | } 664 | 665 | /** 666 | Close the watching abilities of this watcher and its children if it has any 667 | And mark the state as deleted or closed, dependning on the reason 668 | @param {string} [reason='unknown'] 669 | @returns {this} 670 | */ 671 | close(reason /* :string */ = 'unknown') { 672 | // Nothing to do? Already closed? 673 | if (this.state !== 'active') return this 674 | 675 | // Close 676 | this.log('debug', `close: ${this.path}`) 677 | 678 | // Close our children 679 | eachr(this.children, (child) => { 680 | child.close(reason) 681 | }) 682 | 683 | // Close watch listener 684 | if (this.fswatcher != null) { 685 | this.fswatcher.close() 686 | this.fswatcher = null 687 | } else { 688 | fsUtil.unwatchFile(this.path) 689 | } 690 | 691 | // Updated state 692 | if (reason === 'deleted') { 693 | this.state = 'deleted' 694 | } else { 695 | this.state = 'closed' 696 | } 697 | 698 | // Emit our close event 699 | this.log('debug', `watch closed because ${reason} on ${this.path}`) 700 | this.emit('close', reason) 701 | 702 | // Chain 703 | return this 704 | } 705 | 706 | /** 707 | Create the child watcher/stalker for a given sub path of this watcher with inherited configuration 708 | Once created, attach it to `this.children` and bubble `log` and `change` events 709 | If the child closes, then delete it from `this.children` 710 | @private 711 | @param {Object} opts 712 | @param {string} opts.fullPath 713 | @param {string} opts.relativePath 714 | @param {Stats} [opts.stat] 715 | @param {function} next - completion callback with signature `error:?Error` 716 | @returns {this} 717 | */ 718 | watchChild( 719 | opts /* :WatchChildOpts */, 720 | next /* :ErrorCallback */ 721 | ) /* :Stalker */ { 722 | // Prepare 723 | const watchr = this 724 | 725 | // Create the child 726 | const child = create(opts.fullPath) 727 | 728 | // Apply the child 729 | this.children[opts.relativePath] = child 730 | 731 | // Add the extra listaeners 732 | child.once('close', () => delete watchr.children[opts.relativePath]) 733 | child.on('log', (...args) => watchr.emit('log', ...args)) 734 | child.on('change', (...args) => watchr.emit('change', ...args)) 735 | 736 | // Add the extra configuration 737 | child.setConfig({ 738 | // Custom 739 | stat: opts.stat, 740 | 741 | // Inherit 742 | interval: this.config.interval, 743 | persistent: this.config.persistent, 744 | catchupDelay: this.config.catchupDelay, 745 | preferredMethods: this.config.preferredMethods, 746 | ignorePaths: this.config.ignorePaths, 747 | ignoreHiddenFiles: this.config.ignoreHiddenFiles, 748 | ignoreCommonPatterns: this.config.ignoreCommonPatterns, 749 | ignoreCustomPatterns: this.config.ignoreCustomPatterns, 750 | followLinks: this.config.followLinks, 751 | }) 752 | 753 | // Start the watching 754 | child.watch(next) 755 | 756 | // Return the child 757 | return child 758 | } 759 | 760 | /** 761 | Read the directory at our given path and watch each child 762 | @private 763 | @param {Object} opts - not currently used 764 | @param {function} next - completion callback with signature `error:?Error` 765 | @returns {this} 766 | */ 767 | watchChildren(opts /* :Object */, next /* :ErrorCallback */) { 768 | // Prepare 769 | const watchr = this 770 | 771 | // Check stat 772 | if (this.stat == null) { 773 | next(new Error('unexpected state')) 774 | return this 775 | } 776 | 777 | // Cycle through the directory if necessary 778 | const path = this.path 779 | if (this.stat.isDirectory()) { 780 | scandir({ 781 | // Path 782 | path, 783 | 784 | // Options 785 | ignorePaths: this.config.ignorePaths, 786 | ignoreHiddenFiles: this.config.ignoreHiddenFiles, 787 | ignoreCommonPatterns: this.config.ignoreCommonPatterns, 788 | ignoreCustomPatterns: this.config.ignoreCustomPatterns, 789 | recurse: false, 790 | 791 | // Next 792 | next(err, list) { 793 | if (err) return next(err) 794 | const tasks = new TaskGroup(`scandir tasks for ${path}`, { 795 | domain: false, 796 | concurrency: 0, 797 | }).done(next) 798 | Object.keys(list).forEach(function (relativePath) { 799 | tasks.addTask(function (complete) { 800 | const fullPath = pathUtil.join(path, relativePath) 801 | // Check we are still relevant 802 | if (watchr.state !== 'active') return complete() 803 | // Watch this child 804 | watchr.watchChild({ fullPath, relativePath }, complete) 805 | }) 806 | }) 807 | tasks.run() 808 | }, 809 | }) 810 | } else { 811 | next() 812 | } 813 | 814 | // Chain 815 | return this 816 | } 817 | 818 | /** 819 | Setup the watching using the specified method 820 | @private 821 | @param {string} method 822 | @param {function} next - completion callback with signature `error:?Error` 823 | @returns {void} 824 | */ 825 | watchMethod(method /* :MethodEnum */, next /* :ErrorCallback */) /* :void */ { 826 | if (method === 'watch') { 827 | // Check 828 | if (fsUtil.watch == null) { 829 | const err = new Error( 830 | 'watch method is not supported on this environment, fs.watch does not exist' 831 | ) 832 | next(err) 833 | return 834 | } 835 | 836 | // Watch 837 | try { 838 | this.fswatcher = fsUtil.watch(this.path, (...args) => 839 | this.listener({ method, args }) 840 | ) 841 | // must pass the listener here instead of doing fswatcher.on('change', opts.listener) 842 | // as the latter is not supported on node 0.6 (only 0.8+) 843 | } catch (err) { 844 | next(err) 845 | return 846 | } 847 | 848 | // Success 849 | next() 850 | return 851 | } else if (method === 'watchFile') { 852 | // Check 853 | if (fsUtil.watchFile == null) { 854 | const err = new Error( 855 | 'watchFile method is not supported on this environment, fs.watchFile does not exist' 856 | ) 857 | next(err) 858 | return 859 | } 860 | 861 | // Watch 862 | try { 863 | fsUtil.watchFile( 864 | this.path, 865 | { 866 | persistent: this.config.persistent, 867 | interval: this.config.interval, 868 | }, 869 | (...args) => this.listener({ method, args }) 870 | ) 871 | } catch (err) { 872 | next(err) 873 | return 874 | } 875 | 876 | // Success 877 | next() 878 | return 879 | } else { 880 | const err = new Error('unknown watch method') 881 | next(err) 882 | return 883 | } 884 | } 885 | 886 | /** 887 | Setup watching for our path, in the order of the preferred methods 888 | @private 889 | @param {Object} opts 890 | @param {Array} [opts.errors] - the current errors that we have received attempting the preferred methods 891 | @param {Array} [opts.preferredMethods] - fallback to the configuration if not specified 892 | @param {function} next - completion callback with signature `error:?Error` 893 | @returns {this} 894 | */ 895 | watchSelf(opts /* :WatchSelfOpts */, next /* :ErrorCallback */) { 896 | // Prepare 897 | const { errors = [] } = opts 898 | let { preferredMethods = this.config.preferredMethods } = opts 899 | opts.errors = errors 900 | opts.preferredMethods = preferredMethods 901 | 902 | // Attempt the watch methods 903 | if (preferredMethods.length) { 904 | const method = preferredMethods[0] 905 | this.watchMethod(method, (err) => { 906 | if (err) { 907 | // try again with the next preferred method 908 | preferredMethods = preferredMethods.slice(1) 909 | errors.push(err) 910 | this.watchSelf({ errors, preferredMethods }, next) 911 | return 912 | } 913 | 914 | // Apply 915 | this.state = 'active' 916 | 917 | // Forward 918 | next() 919 | }) 920 | } else { 921 | const errors = opts.errors 922 | .map((error) => error.stack || error.message || error) 923 | .join('\n') 924 | const err = new Error( 925 | `no watch methods left to try, failures are:\n${errors}` 926 | ) 927 | next(err) 928 | } 929 | 930 | // Chain 931 | return this 932 | } 933 | 934 | /** 935 | Setup watching for our path, and our children 936 | If we are already watching and `opts.reset` is not `true` then all done 937 | Otherwise, close the current watchers for us and the children via {@link Watcher#close} and setup new ones 938 | @public 939 | @param {Object} [opts] 940 | @param {boolean} [opts.reset=false] - should we always close existing watchers and setup new watchers 941 | @param {function} next - completion callback with signature `error:?Error` 942 | @param {Array<*>} args - ignore this argument, it is used just to handle the optional `opts` argument 943 | @returns {this} 944 | */ 945 | watch(...args /* :Array */) { 946 | // Handle overloaded signature 947 | let opts /* :ResetOpts */, next /* :ErrorCallback */ 948 | if (args.length === 1) { 949 | opts = {} 950 | next = args[0] 951 | } else if (args.length === 2) { 952 | opts = args[0] 953 | next = args[1] 954 | } else { 955 | throw new Error('unknown arguments') 956 | } 957 | 958 | // Check 959 | if (this.state === 'active' && opts.reset !== true) { 960 | next() 961 | return this 962 | } 963 | 964 | // Close our all watch listeners 965 | this.close() 966 | 967 | // Log 968 | this.log('debug', `watch init: ${this.path}`) 969 | 970 | // Fetch the stat then try again 971 | this.getStat({}, (err) => { 972 | if (err) return next(err) 973 | 974 | // Watch ourself 975 | this.watchSelf({}, (err) => { 976 | if (err) return next(err) 977 | 978 | // Watch the children 979 | this.watchChildren({}, (err) => { 980 | if (err) { 981 | this.close('child failure') 982 | this.log( 983 | 'debug', 984 | `watch failed on [${this.path}] with ${errorToString(err)}` 985 | ) 986 | } else { 987 | this.log('debug', `watch success on [${this.path}]`) 988 | } 989 | return next(err) 990 | }) 991 | }) 992 | }) 993 | 994 | // Chain 995 | return this 996 | } 997 | } 998 | 999 | // Now let's provide node.js with our public API 1000 | // In other words, what the application that calls us has access to 1001 | module.exports = { open, create, Stalker, Watcher } 1002 | -------------------------------------------------------------------------------- /source/test.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint no-console:0 no-sync:0 */ 3 | 'use strict' 4 | 5 | // Requires 6 | const pathUtil = require('path') 7 | const fsUtil = require('fs') 8 | const balUtil = require('bal-util') 9 | const rimraf = require('rimraf') 10 | const extendr = require('extendr') 11 | const { equal } = require('assert-helpers') 12 | const { ok } = require('assert') 13 | const kava = require('kava') 14 | const { create } = require('./index') 15 | 16 | // ===================================== 17 | // Configuration 18 | 19 | // Helpers 20 | function wait(delay, fn) { 21 | console.log(`completed, waiting for ${delay}ms delay...`) 22 | return setTimeout(fn, delay) 23 | } 24 | 25 | // Test Data 26 | const batchDelay = 10 * 1000 27 | const fixturesPath = pathUtil.join( 28 | require('os').tmpdir(), 29 | 'watchr', 30 | 'tests', 31 | process.version 32 | ) 33 | const writetree = { 34 | 'a file': 'content of a file', 35 | 'a directory': { 36 | 'a sub file of a directory': 'content of a sub file of a directory', 37 | 'another sub file of a directory': 38 | 'content of another sub file of a directory', 39 | }, 40 | '.a hidden directory': { 41 | 'a sub file of a hidden directory': 42 | 'content of a sub file of a hidden directory', 43 | }, 44 | 'a specific ignored file': 'content of a specific ignored file', 45 | } 46 | 47 | // ===================================== 48 | // Tests 49 | 50 | function runTests(opts, describe, test) { 51 | // Prepare 52 | let stalker = null 53 | 54 | // Change detection 55 | let changes = [] 56 | function checkChanges(expectedChanges, extraTest, next) { 57 | wait(batchDelay, function () { 58 | if (changes.length !== expectedChanges) { 59 | console.log(changes) 60 | } 61 | equal( 62 | changes.length, 63 | expectedChanges, 64 | `${changes.length} changes ran out of ${expectedChanges} changes` 65 | ) 66 | if (extraTest) { 67 | extraTest(changes) 68 | } 69 | changes = [] 70 | next() 71 | }) 72 | } 73 | function changeHappened(...args) { 74 | changes.push(args) 75 | console.log(`a watch event occured: ${changes.length}`, args) 76 | } 77 | 78 | // Files changes 79 | function writeFile(fileRelativePath) { 80 | console.log('write:', fileRelativePath) 81 | const fileFullPath = pathUtil.join(fixturesPath, fileRelativePath) 82 | fsUtil.writeFileSync( 83 | fileFullPath, 84 | `${fileRelativePath} now has the random number ${Math.random()}` 85 | ) 86 | } 87 | function deleteFile(fileRelativePath) { 88 | console.log('delete:', fileRelativePath) 89 | const fileFullPath = pathUtil.join(fixturesPath, fileRelativePath) 90 | fsUtil.unlinkSync(fileFullPath) 91 | } 92 | function makeDir(fileRelativePath) { 93 | console.log('make:', fileRelativePath) 94 | const fileFullPath = pathUtil.join(fixturesPath, fileRelativePath) 95 | fsUtil.mkdirSync(fileFullPath, 0o700) 96 | } 97 | function renameFile(fileRelativePath1, fileRelativePath2) { 98 | console.log('rename:', fileRelativePath1, 'TO', fileRelativePath2) 99 | const fileFullPath1 = pathUtil.join(fixturesPath, fileRelativePath1) 100 | const fileFullPath2 = pathUtil.join(fixturesPath, fileRelativePath2) 101 | fsUtil.renameSync(fileFullPath1, fileFullPath2) 102 | } 103 | 104 | // Tests 105 | test('remove old test files', function (done) { 106 | rimraf(fixturesPath, function (err) { 107 | done(err) 108 | }) 109 | }) 110 | 111 | test('write new test files', function (done) { 112 | balUtil.writetree(fixturesPath, writetree, function (err) { 113 | done(err) 114 | }) 115 | }) 116 | 117 | test('start watching', function (done) { 118 | stalker = create(fixturesPath) 119 | stalker.on('log', console.log) 120 | stalker.on('change', changeHappened) 121 | stalker.setConfig( 122 | extendr.extend( 123 | { 124 | path: fixturesPath, 125 | ignorePaths: [pathUtil.join(fixturesPath, 'a specific ignored file')], 126 | ignoreHiddenFiles: true, 127 | }, 128 | opts 129 | ) 130 | ) 131 | stalker.watch((err) => { 132 | wait(batchDelay, function () { 133 | done(err) 134 | }) 135 | }) 136 | }) 137 | 138 | test('detect write', function (done) { 139 | writeFile('a file') 140 | writeFile('a directory/a sub file of a directory') 141 | checkChanges(2, null, done) 142 | }) 143 | 144 | test('detect write ignored on hidden files', function (done) { 145 | writeFile('.a hidden directory/a sub file of a hidden directory') 146 | checkChanges(0, null, done) 147 | }) 148 | 149 | test('detect write ignored on ignored files', function (done) { 150 | writeFile('a specific ignored file') 151 | checkChanges(0, null, done) 152 | }) 153 | 154 | test('detect delete', function (done) { 155 | deleteFile('a directory/another sub file of a directory') 156 | checkChanges( 157 | 1, 158 | function (changes) { 159 | // make sure previous stat is given 160 | if (!changes[0][3]) { 161 | console.log(changes[0]) 162 | } 163 | ok(changes[0][3], 'previous stat not given to delete') 164 | }, 165 | done 166 | ) 167 | }) 168 | 169 | test('detect delete ignored on hidden files', function (done) { 170 | deleteFile('.a hidden directory/a sub file of a hidden directory') 171 | checkChanges(0, null, done) 172 | }) 173 | 174 | test('detect delete ignored on ignored files', function (done) { 175 | deleteFile('a specific ignored file') 176 | checkChanges(0, null, done) 177 | }) 178 | 179 | test('detect mkdir', function (done) { 180 | makeDir('a new directory') 181 | checkChanges(1, null, done) 182 | }) 183 | 184 | test('detect mkdir and write', function (done) { 185 | writeFile('a new file') 186 | writeFile('another new file') 187 | writeFile('and another new file') 188 | makeDir('another new directory') 189 | checkChanges(4, null, done) 190 | }) 191 | 192 | test('detect rename', function (done) { 193 | renameFile('a new file', 'a new file that was renamed') 194 | checkChanges(2, null, done) // unlink, new 195 | }) 196 | 197 | test('detect subdir file write', function (done) { 198 | writeFile('a new directory/a new file of a new directory') 199 | writeFile('a new directory/another new file of a new directory') 200 | checkChanges(2, null, done) 201 | }) 202 | 203 | test('detect subdir file delete', function (done) { 204 | deleteFile('a new directory/another new file of a new directory') 205 | checkChanges(1, null, done) 206 | }) 207 | 208 | test('stop watching', function () { 209 | if (stalker) { 210 | stalker.close() 211 | } else { 212 | throw new Error('unexpected state') 213 | } 214 | }) 215 | } 216 | 217 | // Run tests for each method 218 | kava.describe('watchr', function (describe) { 219 | describe('watch', function (describe, test) { 220 | runTests({ preferredMethods: ['watch', 'watchFile'] }, describe, test) 221 | }) 222 | describe('watchFile', function (describe, test) { 223 | runTests({ preferredMethods: ['watchFile', 'watch'] }, describe, test) 224 | }) 225 | }) 226 | --------------------------------------------------------------------------------