diff --git a/README.md b/README.md index a005f50..7174b23 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ watchpack high level API doesn't map directly to watchers. Instead a three level - The real watchers are created by the `DirectoryWatcher`. - Files are never watched directly. This should keep the watcher count low. - Watching can be started in the past. This way watching can start after file reading. -- Symlinks are not followed, instead the symlink is watched. ## API diff --git a/lib/DirectoryWatcher.js b/lib/DirectoryWatcher.js index 034447d..efcc953 100644 --- a/lib/DirectoryWatcher.js +++ b/lib/DirectoryWatcher.js @@ -1,4 +1,4 @@ -/* +/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ @@ -61,6 +61,7 @@ class DirectoryWatcher extends EventEmitter { this.watcherManager = watcherManager; this.options = options; this.path = directoryPath; + this.watchingSymlink = false; // safeTime is the point in time after which reading is safe to be unchanged // timestamp is a value that should be compared with another timestamp (mtime) /** @type {Map { if (this.closed) return; this._activeEvents.set(filename, false); + fs.lstat(filePath, (err, stats) => { if (this.closed) return; if (this._activeEvents.get(filename) === true) { @@ -549,11 +551,6 @@ class DirectoryWatcher extends EventEmitter { fs.readdir(this.path, (err, items) => { if (this.closed) return; if (err) { - if (err.code === "ENOENT" || err.code === "EPERM") { - this.onDirectoryRemoved("scan readdir failed"); - } else { - this.onScanError(err); - } this.initialScan = false; this.initialScanFinished = Date.now(); if (initial) { @@ -626,22 +623,7 @@ class DirectoryWatcher extends EventEmitter { } }); for (const itemPath of itemPaths) { - fs.lstat(itemPath, (err2, stats) => { - if (this.closed) return; - if (err2) { - if ( - err2.code === "ENOENT" || - err2.code === "EPERM" || - err2.code === "EACCES" || - err2.code === "EBUSY" - ) { - this.setMissing(itemPath, initial, "scan (" + err2.code + ")"); - } else { - this.onScanError(err2); - } - itemFinished(); - return; - } + const handleStats = (stats, symlinkStats) => { if (stats.isFile() || stats.isSymbolicLink()) { if (stats.mtime) { ensureFsAccuracy(stats.mtime); @@ -653,7 +635,11 @@ class DirectoryWatcher extends EventEmitter { true, "scan (file)" ); - } else if (stats.isDirectory()) { + } + if ( + stats.isDirectory() || + (symlinkStats && symlinkStats.isDirectory()) + ) { if (!initial || !this.directories.has(itemPath)) this.setDirectory( itemPath, @@ -663,6 +649,42 @@ class DirectoryWatcher extends EventEmitter { ); } itemFinished(); + }; + fs.lstat(itemPath, (err2, stats) => { + if (this.closed) return; + if (err2) { + if ( + err2.code === "ENOENT" || + err2.code === "EPERM" || + err2.code === "EACCES" || + err2.code === "EBUSY" + ) { + this.setMissing(itemPath, initial, "scan (" + err2.code + ")"); + } else { + this.onScanError(err2); + } + itemFinished(); + return; + } + if ( + stats.isSymbolicLink() && + this.watcherManager.options.followSymlinks + ) { + fs.stat(itemPath, (err3, symlinkStats) => { + if (this.closed) return; + // something is wrong with the symlink, but not with the file itself + if (err3) { + handleStats(stats); + this.watchingSymlink = false; + return; + } + this.watchingSymlink = true; + handleStats(stats, symlinkStats); + }); + } else { + this.watchingSymlink = false; + handleStats(stats); + } }); } itemFinished(); diff --git a/test/Watchpack.js b/test/Watchpack.js index 4559c62..a1963ba 100644 --- a/test/Watchpack.js +++ b/test/Watchpack.js @@ -1237,6 +1237,8 @@ describe("Watchpack", function() { testHelper.symlinkFile(path.join("a", "b", "link"), "c"); testHelper.symlinkFile(path.join("a", "b", "link2"), "link"); testHelper.symlinkFile("link2", "link/link/link2"); + testHelper.dir("b"); + testHelper.symlinkDir(path.join("b", "link"), path.join("..", "a", "b")); testHelper.tick(1000, done); }); @@ -1368,6 +1370,20 @@ describe("Watchpack", function() { } ); }); + + it("should detect a change to symlinked file outside watched directory", function(done) { + expectWatchEvent( + [], + path.join(fixtures, "b"), + changes => { + Array.from(changes).should.be.eql([path.join(fixtures, "b")]); + done(); + }, + () => { + testHelper.file(path.join("a", "b", "d")); + } + ); + }); }); } else { it("symlinks");