Comparing version 0.0.3 to 0.0.4
229
index.js
const path = require('path'); | ||
const EventEmitter = require('events'); | ||
const fs = require('fs'); | ||
const globby = require('globby'); | ||
const diff = require('lodash.difference'); | ||
const through = require('through2'); | ||
const unixify = require('unixify'); | ||
const dirGlob = require('dir-glob'); | ||
const micromatch = require('micromatch'); | ||
const pify = require('pify'); | ||
const readdir = (dir, pattern) => { | ||
return globby(pattern, { | ||
cwd: dir, | ||
deep: 1, | ||
onlyFiles: false, | ||
markDirectories: true | ||
const pReaddir = pify(fs.readdir); | ||
const pStat = pify(fs.stat); | ||
const readdir = async (dir) => { | ||
dir = dir.slice(-1) === '/' ? dir : `${dir}/`; | ||
let result; | ||
if (fs.Dirent) { | ||
result = await pReaddir(dir, { withFileTypes: true }); | ||
} else { | ||
const list = await pReaddir(dir); | ||
result = []; | ||
for (let name of list) { | ||
result.push(Object.assign(await pStat(`${dir}${name}`), { name })); | ||
} | ||
} | ||
return result.map(dirent => { | ||
if (dirent.isDirectory()) { | ||
return `${dir}${dirent.name}/`; | ||
} else { | ||
return `${dir}${dirent.name}`; | ||
} | ||
}); | ||
}; | ||
const exists = (abspath) => { | ||
return new Promise(r => fs.access(abspath, err => r(!err))); | ||
const isMatch = (input, patterns) => { | ||
let failed = false; | ||
for (let p of patterns) { | ||
failed = failed || !micromatch.isMatch(input, p); | ||
} | ||
return !failed; | ||
}; | ||
const iterateStream = (stream, iterate) => { | ||
return new Promise((resolve, reject) => { | ||
stream.on('error', err => reject(err)); | ||
const isParent = (input, patterns) => { | ||
for (let p of patterns) { | ||
if (p.indexOf(input) === 0) { | ||
return true; | ||
} | ||
} | ||
stream.pipe(through.obj((data, enc, cb) => { | ||
iterate(data).then(() => { | ||
cb(); | ||
}).catch(err => { | ||
cb(err); | ||
}); | ||
})) | ||
.on('data', () => {}) | ||
.on('end', () => resolve()) | ||
.on('error', err => reject(err)); | ||
}); | ||
return false; | ||
}; | ||
const globdir = async (dir, patterns) => { | ||
const run = async () => { | ||
const entries = await readdir(dir); | ||
const matches = entries.filter((e) => isMatch(e, patterns) || isParent(e, patterns)); | ||
return matches; | ||
}; | ||
if (fs.Dirent) { | ||
return await run(); | ||
} | ||
// node 8 has this nasty habbit of returning 0 entries on a readdir | ||
// directly after a change even when there are entries, so we need | ||
// to confirm that two runs read the same amount of entries | ||
const one = await run(); | ||
const two = await run(); | ||
if (one.length === two.length) { | ||
return two; | ||
} | ||
return globdir(dir, patterns); | ||
}; | ||
const exists = (abspath) => { | ||
return new Promise(r => fs.access(abspath, err => r(!err))); | ||
}; | ||
const evMap = { | ||
@@ -46,2 +95,7 @@ change: 1, | ||
const addDriveLetter = (basePath, str) => { | ||
const drive = (basePath.match(/^([a-z]:)\\/i) || [])[1]; | ||
return drive ? `${drive}${str}` : str; | ||
}; | ||
module.exports = (pattern, { | ||
@@ -51,2 +105,18 @@ cwd = process.cwd(), | ||
} = {}) => { | ||
// support passing relative paths and '.' | ||
cwd = path.resolve(cwd); | ||
const resolvedPatterns = (Array.isArray(pattern) ? pattern : [pattern]).map(str => { | ||
const negative = str[0] === '!'; | ||
if (negative) { | ||
str = str.slice(1); | ||
} | ||
const absPattern = addDriveLetter(cwd, path.posix.resolve(unixify(cwd), str)); | ||
return negative ? `!${absPattern}` : absPattern; | ||
}); | ||
let absolutePatterns; | ||
const events = new EventEmitter(); | ||
@@ -63,9 +133,5 @@ const dirs = {}; | ||
const funcKey = `func : ${abspath}`; | ||
if (pending[funcKey]) { | ||
clearTimeout(pending[funcKey]); | ||
} else { | ||
// save only the first set of arguments | ||
pending[abspath] = { evname, evarg, priority: evMap[evname] || 0 }; | ||
if (!pending[abspath]) { | ||
// save the first set of arguments | ||
pending[abspath] = { evname, evarg, priority: evMap[evname] || 0, timer: null }; | ||
} | ||
@@ -75,11 +141,41 @@ | ||
// this event takes precedence over the queued one | ||
pending[abspath] = { evname, evarg, priority: evMap[evname] || 0 }; | ||
pending[abspath].evname = evname; | ||
pending[abspath].evarg = evarg; | ||
pending[abspath].priority = evMap[evname] || 0; | ||
} | ||
pending[funcKey] = setTimeout(() => { | ||
if (pending[abspath].timer) { | ||
clearTimeout(pending[abspath].timer); | ||
} | ||
pending[abspath].timer = setTimeout(() => { | ||
if (closed) { | ||
delete pending[abspath]; | ||
return; | ||
} | ||
const { evname, evarg } = pending[abspath]; | ||
events.emit(evname, evarg); | ||
delete pending[abspath]; | ||
delete pending[funcKey]; | ||
if (evname !== 'change') { | ||
delete pending[abspath]; | ||
return void events.emit(evname, evarg); | ||
} | ||
// always check that this file exists on a change event due to a bug | ||
// in node 12 that fires a delete as a change instead of rename | ||
// https://github.com/nodejs/node/issues/27869 | ||
exists(abspath).then(yes => { | ||
if (closed) { | ||
delete pending[abspath]; | ||
return; | ||
} | ||
// it is possible file could have been deleted during the check | ||
const { evname, evarg } = pending[abspath]; | ||
delete pending[abspath]; | ||
events.emit(yes ? evname : 'unlink', evarg); | ||
}).catch(err => { | ||
error(err, abspath); | ||
}); | ||
}, 50); | ||
@@ -93,3 +189,3 @@ }; | ||
err.path = abspath; | ||
err.path = path.resolve(abspath); | ||
@@ -105,3 +201,3 @@ events.emit('error', err); | ||
delete files[abspath]; | ||
throttle(abspath, 'unlink', { path: abspath }); | ||
throttle(abspath, 'unlink', { path: path.resolve(abspath) }); | ||
} | ||
@@ -116,12 +212,8 @@ }; | ||
delete dirs[abspath]; | ||
throttle(abspath, 'unlinkDir', { path: abspath }); | ||
throttle(abspath, 'unlinkDir', { path: path.resolve(abspath) }); | ||
} | ||
}; | ||
const onFileChange = (abspath) => (type) => { | ||
if (type === 'rename') { | ||
return removeFile(abspath); | ||
} | ||
throttle(abspath, 'change', { path: abspath }); | ||
const onFileChange = (abspath) => () => { | ||
throttle(abspath, 'change', { path: path.resolve(abspath) }); | ||
}; | ||
@@ -131,8 +223,8 @@ | ||
try { | ||
const paths = await readdir(abspath, pattern); | ||
const paths = await globdir(abspath, absolutePatterns); | ||
const [foundFiles, foundDirs] = paths.reduce(([files, dirs], file) => { | ||
if (/\/$/.test(file)) { | ||
dirs.push(path.resolve(abspath, file)); | ||
dirs.push(file.slice(0, -1)); | ||
} else { | ||
files.push(path.resolve(abspath, file)); | ||
files.push(file); | ||
} | ||
@@ -145,4 +237,3 @@ | ||
const existingFiles = Object.keys(files) | ||
.filter(file => path.dirname(file) === abspath) | ||
.filter(file => !dirs[file]); | ||
.filter(file => path.posix.dirname(file) === abspath); | ||
// diff returns items in the first array that are not in the second | ||
@@ -154,9 +245,9 @@ diff(existingFiles, foundFiles).forEach(file => removeFile(file)); | ||
const existingDirs = Object.keys(dirs) | ||
.filter(dir => path.dirname(dir) === abspath) | ||
.filter(dir => !files[dir]); | ||
.filter(dir => path.posix.dirname(dir) === abspath); | ||
diff(existingDirs, foundDirs).forEach(dir => removeDir(dir)); | ||
diff(foundDirs, existingDirs).forEach(dir => { | ||
watchDir(dir); | ||
}); | ||
for (let dir of diff(foundDirs, existingDirs)) { | ||
await watchDir(dir); | ||
} | ||
} catch (err) { | ||
@@ -187,3 +278,3 @@ try { | ||
events.emit('add', { path: abspath }); | ||
events.emit('add', { path: path.resolve(abspath) }); | ||
}; | ||
@@ -204,22 +295,18 @@ | ||
return onDirChange(abspath)().then(() => { | ||
events.emit('addDir', { path: abspath }); | ||
events.emit('addDir', { path: path.resolve(abspath) }); | ||
}); | ||
}; | ||
iterateStream(globby.stream(pattern, { | ||
onlyFiles: false, | ||
markDirectories: true, | ||
cwd, | ||
concurrency: 1 | ||
}), async (file) => { | ||
const abspath = path.resolve(cwd, file); | ||
if (/\/$/.test(file)) { | ||
await watchDir(abspath); | ||
} else { | ||
await watchFile(abspath); | ||
} | ||
dirGlob(resolvedPatterns, { cwd }).then((p) => { | ||
absolutePatterns = p; | ||
}).then(() => { | ||
return watchDir(cwd); | ||
const dir = addDriveLetter(cwd, unixify(cwd)); | ||
return watchDir(dir); | ||
}).then(() => { | ||
// this is the most annoying part, but it seems that watching does not | ||
// occur immediately, yet there is no event for whenan fs watcher is | ||
// actually ready... some of the internal bits use process.nextTick, | ||
// so we'll wait a very random sad small amount of time here | ||
return new Promise(r => setTimeout(() => r(), 20)); | ||
}).then(() => { | ||
events.emit('ready'); | ||
@@ -226,0 +313,0 @@ }).catch(err => { |
{ | ||
"name": "watchboy", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"description": "watchboy", | ||
@@ -21,5 +21,7 @@ "main": "index.js", | ||
"dependencies": { | ||
"globby": "^10.0.1", | ||
"dir-glob": "^3.0.1", | ||
"lodash.difference": "^4.5.0", | ||
"through2": "^3.0.1" | ||
"micromatch": "^4.0.2", | ||
"pify": "^4.0.1", | ||
"unixify": "^1.0.0" | ||
}, | ||
@@ -26,0 +28,0 @@ "devDependencies": { |
# watchboy | ||
Watch files and directories for changes. Fast. No hassle. No native dependencies. Works the same way on Windows, Linux, and MacOS. Low memory usage. Everything you've ever wanted in a module. | ||
[![watchboy logo](https://cdn.jsdelivr.net/gh/catdad-experiments/catdad-experiments-org@7005ab/watchboy/logo.jpg)](https://github.com/catdad/watchboy/) | ||
[![travis][travis.svg]][travis.link] | ||
[![npm-downloads][npm-downloads.svg]][npm.link] | ||
[![npm-version][npm-version.svg]][npm.link] | ||
[![dm-david][dm-david.svg]][dm-david.link] | ||
[travis.svg]: https://travis-ci.com/catdad/watchboy.svg?branch=master | ||
[travis.link]: https://travis-ci.com/catdad/watchboy | ||
[npm-downloads.svg]: https://img.shields.io/npm/dm/watchboy.svg | ||
[npm.link]: https://www.npmjs.com/package/watchboy | ||
[npm-version.svg]: https://img.shields.io/npm/v/watchboy.svg | ||
[dm-david.svg]: https://david-dm.org/catdad/watchboy.svg | ||
[dm-david.link]: https://david-dm.org/catdad/watchboy | ||
Watch files and directories for changes. Fast. No hassle. No native dependencies. Works the same way on Windows, Linux, and MacOS. Low memory usage. Shows you a picture of a dog. It's everything you've ever wanted in a module! | ||
## Install | ||
@@ -26,3 +41,3 @@ | ||
watcher.on('ready', () => console.log('all initial files and directories found')); | ||
watcher.on('errpr', err => console.error('watcher error:', err)); | ||
watcher.on('error', err => console.error('watcher error:', err)); | ||
@@ -36,3 +51,3 @@ // stop all watching | ||
### `watchboy(pattern, [options])` → `EventEmitter` | ||
### `watchboy(pattern, [options])` → [`EventEmitter`] | ||
@@ -47,27 +62,27 @@ Watchboy is exposed as a function which returns an event emitter. It takes the following parameters: | ||
### `.on('add', ({ path }) => {})` → `EventEmitter` | ||
### `.on('add', ({ path }) => {})` → [`EventEmitter`] | ||
Indicates that a new file was added. There is a single argument for this event, which has a `path` property containing the absolute path for the file that was added. | ||
### `.on('addDir', ({ path }) => {})` → `EventEmitter` | ||
### `.on('addDir', ({ path }) => {})` → [`EventEmitter`] | ||
Indicates that a new directory was added. There is a single argument for this event, which has a `path` property containing the absolute path for the directory that was added. Files in this new directory will also be watched according to the provided patterns. | ||
### `.on('change', ({ path }) => {})` → `EventEmitter` | ||
### `.on('change', ({ path }) => {})` → [`EventEmitter`] | ||
Indicates that a file has changed. There is a single argument for this event, which has a `path` property containing the absolute path for the file that has changed. | ||
### `.on('unlink', ({ path }) => {})` → `EventEmitter` | ||
### `.on('unlink', ({ path }) => {})` → [`EventEmitter`] | ||
Indicates that a watched file no longer exists. There is a single argument for this event, which has a `path` property containing the absolute path for the file that no longer exists. | ||
### `.on('unlinkDir', ({ path }) => {})` → `EventEmitter` | ||
### `.on('unlinkDir', ({ path }) => {})` → [`EventEmitter`] | ||
Indicates that a watched directory no longer exists. There is a single argument for this event, which has a `path` property containing the absolute path for the directory that no longer exists. | ||
### `.on('ready', () => {})` → `EventEmitter` | ||
### `.on('ready', () => {})` → [`EventEmitter`] | ||
Indicates that all initial files and directories have been discovered. This even has no arguments. Note that new `add` and `addDir` events may fire after this, as new files and directories that match the patterns are created. | ||
### `.on('error', (err) => {})` → `EventEmitter` | ||
### `.on('error', (err) => {})` → [`EventEmitter`] | ||
@@ -79,1 +94,7 @@ Indicates that an error has occurred. You must handle this event so that your application does not crash. This error has a single argument: an error which indicates what happened. Aside from standard error properties, there is an additional `path` property indicating the absolute path of the file or directory which triggered the error. | ||
Stop watching all files. After this method is called, the watcher can no longer be used and no more events will fire. | ||
[`EventEmitter`]: https://nodejs.org/api/events.html#events_class_eventemitter | ||
## Performance | ||
Check out [this benchmark](https://github.com/catdad-experiments/filewatch-benchmarks) comparing `watchboy` to popular alternatives. Spoiler: it fairs really well. |
@@ -62,1 +62,4 @@ /* eslint-disable no-console */ | ||
// * new files in new subdirectories are watched | ||
// cd coverage | ||
// node ..\test\harness.js "**/*" "!lcov-report" |
@@ -9,4 +9,27 @@ /* eslint-env mocha */ | ||
const watchboy = require(root); | ||
const log = (...args) => { | ||
if (process.env.TEST_DEBUG) { | ||
// eslint-disable-next-line no-console | ||
console.log(...args); | ||
} | ||
}; | ||
const watchboy = (() => { | ||
const lib = require(root); | ||
return (...args) => { | ||
const watcher = lib(...args); | ||
watcher.on('add', ({ path }) => log('add:', path)); | ||
watcher.on('addDir', ({ path }) => log('addDir:', path)); | ||
watcher.on('change', ({ path }) => log('change:', path)); | ||
watcher.on('unlink', ({ path }) => log('unlink:', path)); | ||
watcher.on('unlinkDir', ({ path }) => log('unlinkDir:', path)); | ||
watcher.on('ready', () => log('ready')); | ||
watcher.on('error', err => log('watcher error:', err.message)); | ||
return watcher; | ||
}; | ||
})(); | ||
describe('watchboy', () => { | ||
@@ -26,3 +49,3 @@ const temp = path.resolve(root, 'temp'); | ||
file('pineapples/six.txt'), | ||
].map(f => fs.outputFile(f, Math.random().toString(36)))); | ||
].map(f => fs.outputFile(f, ''))); | ||
}); | ||
@@ -125,3 +148,3 @@ afterEach(async () => { | ||
}), | ||
fs.outputFile(testFile, Math.random().toString(36)) | ||
fs.outputFile(testFile, '') | ||
]); | ||
@@ -150,3 +173,3 @@ | ||
it('emits an "add" and "addDir" when a new file is added to a new directory in an already watched directory', async () => { | ||
const testFile = file('kiwi/seven.txt'); | ||
const testFile = file('pineapple/wedges/seven.txt'); | ||
@@ -164,3 +187,3 @@ await new Promise(r => { | ||
}), | ||
fs.outputFile(testFile, Math.random().toString(36)) | ||
fs.outputFile(testFile, '') | ||
]); | ||
@@ -181,2 +204,48 @@ | ||
it('watches a nested pattern', async () => { | ||
await new Promise(r => { | ||
watcher = watchboy('pineapples/**/*', { cwd: temp, persistent: false }).on('ready', () => r()); | ||
}); | ||
const actualAddedFile = file('pineapples/seven.txt'); | ||
const [addedFile] = await Promise.all([ | ||
new Promise(r => { | ||
watcher.once('add', ({ path }) => r(path)); | ||
}), | ||
fs.outputFile(actualAddedFile, '') | ||
]); | ||
expect(addedFile).to.equal(actualAddedFile); | ||
const actualChangedFile = file('pineapples/six.txt'); | ||
const [changedFile] = await Promise.all([ | ||
new Promise(r => { | ||
watcher.once('change', ({ path }) => r(path)); | ||
}), | ||
touch(actualChangedFile) | ||
]); | ||
expect(changedFile).to.equal(actualChangedFile); | ||
const actualAddedDir = file('pineapples/slices'); | ||
const [addedDir] = await Promise.all([ | ||
new Promise(r => { | ||
watcher.once('addDir', ({ path }) => r(path)); | ||
}), | ||
fs.ensureDir(actualAddedDir) | ||
]); | ||
expect(addedDir).to.equal(actualAddedDir); | ||
const actualNestedFile = file('pineapples/slices/eight.txt'); | ||
const [nestedFile] = await Promise.all([ | ||
new Promise(r => { | ||
watcher.once('add', ({ path }) => r(path)); | ||
}), | ||
fs.outputFile(actualNestedFile, '') | ||
]); | ||
expect(nestedFile).to.equal(actualNestedFile); | ||
}); | ||
describe('close', () => { | ||
@@ -183,0 +252,0 @@ it('stops all listeners'); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
25640
566
97
0
5
4
+ Addeddir-glob@^3.0.1
+ Addedmicromatch@^4.0.2
+ Addedpify@^4.0.1
+ Addedunixify@^1.0.0
+ Addednormalize-path@2.1.1(transitive)
+ Addedpify@4.0.1(transitive)
+ Addedremove-trailing-separator@1.1.0(transitive)
+ Addedunixify@1.0.0(transitive)
- Removedglobby@^10.0.1
- Removedthrough2@^3.0.1
- Removed@nodelib/fs.scandir@2.1.5(transitive)
- Removed@nodelib/fs.stat@2.0.5(transitive)
- Removed@nodelib/fs.walk@1.2.8(transitive)
- Removed@types/glob@7.2.0(transitive)
- Removed@types/minimatch@5.1.2(transitive)
- Removed@types/node@22.8.1(transitive)
- Removedarray-union@2.1.0(transitive)
- Removedbalanced-match@1.0.2(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removedfast-glob@3.3.2(transitive)
- Removedfastq@1.17.1(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedglob@7.2.3(transitive)
- Removedglob-parent@5.1.2(transitive)
- Removedglobby@10.0.2(transitive)
- Removedignore@5.3.2(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinherits@2.0.4(transitive)
- Removedis-extglob@2.1.1(transitive)
- Removedis-glob@4.0.3(transitive)
- Removedmerge2@1.4.1(transitive)
- Removedminimatch@3.1.2(transitive)
- Removedonce@1.4.0(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedqueue-microtask@1.2.3(transitive)
- Removedreadable-stream@3.6.2(transitive)
- Removedreusify@1.0.4(transitive)
- Removedrun-parallel@1.2.0(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedslash@3.0.0(transitive)
- Removedstring_decoder@1.3.0(transitive)
- Removedthrough2@3.0.2(transitive)
- Removedundici-types@6.19.8(transitive)
- Removedutil-deprecate@1.0.2(transitive)
- Removedwrappy@1.0.2(transitive)