Have you used AI?
Yes
Bug Description
On Linux, watching a file through a dangling directory symlink with followSymlinks: false (the default) causes Watchpack to repeatedly call fs.watch while the target directory is absent, instead of waiting for it to become available.
Link to Minimal Reproduction and step to reproduce
Steps:
npm install watchpack@2.5.2
- Save the reproducer as
repro.js
- Run
node repro.js
- Observe the reported
fs.watch attempt count
const fs = require("fs"), os = require("os"), path = require("path"), Watchpack = require("watchpack");
// Count every fs.watch call Watchpack makes.
let attempts = 0;
const realWatch = fs.watch;
fs.watch = (...args) => { attempts++; return realWatch(...args); };
// Create a dangling symlink and watch a file behind it.
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "watchpack-dangling-"));
const target = path.join(dir, "target");
const link = path.join(dir, "link");
const file = path.join(link, "file.txt");
fs.symlinkSync(target, link);
const watcher = new Watchpack({ followSymlinks: false });
let targetCreated = false;
watcher.on("change", changed => { targetCreated = changed === file; });
watcher.watch({ files: [file] });
// Create the missing target after 1 second.
setTimeout(() => {
fs.mkdirSync(target);
fs.writeFileSync(path.join(target, "file.txt"), "created");
}, 1000);
// Stop after 3 seconds and report the result.
setTimeout(() => {
watcher.close();
fs.rmSync(dir, { recursive: true, force: true });
console.log(`fs.watch attempts: ${attempts}`);
console.log(`target creation reported: ${targetCreated}`);
process.exitCode = attempts > 20 || !targetCreated ? 1 : 0;
}, 3000);
Expected Behavior
Watchpack should keep retry behaviour bounded while the target is absent, avoid a tight loop of fs.watch calls, and resume watching when the requested file becomes available.
Actual Behavior
Using upstream source at 2b27b7d on Linux with Node v24.14.0, the three-second run recorded approximately 16,405 fs.watch attempts. Target creation was also reported. The script creates the target after one second and stops after three seconds; the count is the total for the run.
The installation steps above use the published watchpack@2.5.2 package. The recorded Linux count is from the upstream source checkout, which is a separate revision from that release.
On macOS, the same script recorded 8 attempts and no target-creation notification during the three-second test.
Environment
System:
OS: Linux 7.0.0-1012-aws x86_64
Binaries:
Node: v24.14.0
npm: 11.9.0
Watchpack:
npm release in reproduction instructions: 2.5.2
upstream revision used for recorded Linux count: 2b27b7d
Is this a regression?
No
We have not established whether this worked in an earlier version.
Last Working Version
Unknown.
Additional Context
We encountered this in a Rspack development server in a Bazel-based workspace. Rspack bundles Watchpack, and Bazel can create dangling directory symlinks that later become valid.
High CPU usage and V8 OOMs were reported in affected larger builds. This minimal example measures watch attempts, not CPU utilisation or memory consumption, and does not establish the cause of those OOMs.
Setting followSymlinks: true avoided the tight retry loop in the tested Linux scenario. However, it watches both symlinks and their targets, changing watcher behaviour, so it is a workaround rather than an equivalent fix.
For historical context, a comment on #185 describes a LinkResolver ENOENT failure involving Bazel symlink paths. That is a different failure mode from the retry loop reported here.
Have you used AI?
Yes
Bug Description
On Linux, watching a file through a dangling directory symlink with
followSymlinks: false(the default) causes Watchpack to repeatedly callfs.watchwhile the target directory is absent, instead of waiting for it to become available.Link to Minimal Reproduction and step to reproduce
Steps:
npm install watchpack@2.5.2repro.jsnode repro.jsfs.watchattempt countExpected Behavior
Watchpack should keep retry behaviour bounded while the target is absent, avoid a tight loop of
fs.watchcalls, and resume watching when the requested file becomes available.Actual Behavior
Using upstream source at
2b27b7don Linux with Nodev24.14.0, the three-second run recorded approximately 16,405fs.watchattempts. Target creation was also reported. The script creates the target after one second and stops after three seconds; the count is the total for the run.The installation steps above use the published
watchpack@2.5.2package. The recorded Linux count is from the upstream source checkout, which is a separate revision from that release.On macOS, the same script recorded 8 attempts and no target-creation notification during the three-second test.
Environment
Is this a regression?
No
We have not established whether this worked in an earlier version.
Last Working Version
Unknown.
Additional Context
We encountered this in a Rspack development server in a Bazel-based workspace. Rspack bundles Watchpack, and Bazel can create dangling directory symlinks that later become valid.
High CPU usage and V8 OOMs were reported in affected larger builds. This minimal example measures watch attempts, not CPU utilisation or memory consumption, and does not establish the cause of those OOMs.
Setting
followSymlinks: trueavoided the tight retry loop in the tested Linux scenario. However, it watches both symlinks and their targets, changing watcher behaviour, so it is a workaround rather than an equivalent fix.For historical context, a comment on #185 describes a
LinkResolverENOENTfailure involving Bazel symlink paths. That is a different failure mode from the retry loop reported here.