Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Arnesfield
Copy link
Contributor

@Arnesfield Arnesfield commented Jun 18, 2025

Glob paths are no longer supported by chokidar starting at v4.

This update works around this by resolving watchFiles and watchIgnore to valid paths and creating a list of matchers to determine if the files should be allowed or ignored through the chokidar ignored match function.

This commit reverts changes from 8af0f1a so that the watched file changes are not fixed to the current directory.

Additional note: when a watchFile path is removed while chokidar is watching it, recreating the watchFile path does not trigger events from chokidar to rerun the tests.

PR Checklist

Overview

Glob paths are no longer supported by chokidar starting at v4.

This update works around this by resolving `watchFiles` and `watchIgnore`
to valid paths and creating a list of matchers to determine if the files
should be allowed or ignored through the chokidar `ignored` match function.

This commit reverts changes from 8af0f1a
so that the watched file changes are not fixed to the current directory.

Additional note: when a `watchFile` path is removed while chokidar is
watching it, recreating the `watchFile` path does not trigger events
from chokidar to rerun the tests.
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Jun 18, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

@Arnesfield Arnesfield changed the title fix: watch mode using chokidar v4 (#5355) [WIP] fix: watch mode using chokidar v4 (#5355) Jun 18, 2025
@Arnesfield
Copy link
Contributor Author

Might need to rethink this a bit. Currently, running into some issues:

  • The test reruns test when file matching --watch-files is added doesn't pass
  • The watchFiles for this test are: **/*.xyz
  • From what I understand, the actual test result looks like it has an additional rerun (3 instead of the expected 2) which I think is probably due to the directory being created (lib/ then lib/file.xyz)
  • But ignoring the directory in chokidar means its contents are also ignored
  • Need a way to not ignore a directory (lib) but also only allow contents to trigger a rerun (*.xyz)

@JoshuaKGoldberg JoshuaKGoldberg marked this pull request as draft June 18, 2025 15:00
This update fixes mochajs#5355 and refactors the previous watch mode fix attempt.

Instead of only filtering the paths in the chokidar `ignored` match
function, the chokidar `all` event handler now has a guard to ensure
that only file paths that match the allowed patterns from `watchFiles`
would trigger test reruns.

Doing this solves the issue where creating/deleting directories trigger
test reruns despite the watched path being a file (e.g., `**/*.xyz`)
as the allowed patterns would not match with just the directory paths.
@Arnesfield Arnesfield changed the title [WIP] fix: watch mode using chokidar v4 (#5355) fix: watch mode using chokidar v4 (#5355) Jun 19, 2025
@Arnesfield Arnesfield marked this pull request as ready for review June 19, 2025 12:32
Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes seem reasonable to me at first, but they're not covered by tests at all. I think we'd want to have at least one integration test covering the behavior.

Maybe:

  1. Programmatically creates a big enough test case project to crash chokidar v4's watch mode
  2. Starts Mocha in watch mode
  3. Waits until it logs the expected message (and asserts it doesn't log an EMFILE or anything like that)
  4. Closes Mocha

WDYT?

@JoshuaKGoldberg JoshuaKGoldberg added the status: waiting for author waiting on response from OP or other posters - more information needed label Jul 2, 2025
@Arnesfield
Copy link
Contributor Author

Arnesfield commented Jul 2, 2025

Hi @JoshuaKGoldberg, thanks for the response.

Actually, the changes are really only meant for handling glob paths for watchFiles and watchIgnore and not so much about the EMFILE issue or crashing chokidar. I'm thinking that those issues should have their own pull requests and set of changes instead of here.

To recap, since chokidar v4 dropped support for globs, this caused issues where files are not correctly being watched. Before 8af0f1a, the watchFiles were being passed to chokidar directly, essentially entrusting the glob path handling to chokidar. After 8af0f1a, which upgrades chokidar to v4, these occurred:

  • Glob paths for watchFiles and watchIgnore no longer work as expected.
  • The watcher instance is now only watching file changes on the current directory (chokidar.watch('.')) instead of the patterns from watchFiles.
  • The watcher instance no longer ignores patterns from watchIgnore.
  • Watch mode become noticeably slower to start, probably due to the glob.sync() calls, especially for the watchIgnore patterns which would normally include node_modules.

These are the issues that these changes aim to address which are mostly fixes for the watched files, and I think that the EMFILE issue or testing the limits of chokidar would be out of scope for this (even though they're still more or less related to the watch mode).

While the watchIgnore patterns being ignored with this update could probably help with the EMFILE issue, the issues mentioned could still occur if the user's machine has a lower resource limit or if it does have too many files being read (e.g., by another program), and those could be caused by something outside of mocha/chokidar.

Please let me know what you think. Thanks!

@JoshuaKGoldberg
Copy link
Member

The assorted recent watch mode issues for reference:

If it's easiest to fix >=1 of these all together in one PR (as in, the changed areas of code intersect a lot). I'd say go for it. 🙂

@Arnesfield
Copy link
Contributor Author

The list of issues seem about right to me, where the first 2 are more or less about the watch mode taking some time to start than before and the last being the main issue. I think the changes here now should already address these issues, unless I missed something. Let me know should we need to update a few things or if there's anything to do on my end, thanks!

@JoshuaKGoldberg
Copy link
Member

Great! Can you add test(s) to verify the changed behavior? #5379 (review)

- Test for watched files from outside the current working directory
- Test reruns with a thousand watched files
@Arnesfield
Copy link
Contributor Author

Arnesfield commented Jul 3, 2025

Added the following tests:

  • Test for watched files from outside the current working directory
  • Test reruns with a thousand watched files

Though, I'm not too confident with the approach for the last added test. Let me know if we should change how we go about it, thanks.

Edit: Well, the last test seems to have failed since it kind of relied on the machine's performance. I'll work on a fix for it and limit the change to a single file.

@mark-wiemer mark-wiemer self-requested a review July 12, 2025 01:01
@mark-wiemer mark-wiemer removed the status: waiting for author waiting on response from OP or other posters - more information needed label Jul 12, 2025
@mark-wiemer
Copy link
Member

mark-wiemer commented Jul 12, 2025

Next step here will be ensuring the test failures are nothing new (ref #5361), then I can review the code and if I approve I'll ping Josh to do the same :D

The test is dependent on how fast the machine could create 1000 watched
files, otherwise the test could fail due to a timeout error. Reducing
the number of watched files would make the test pass, but doing so would
make it more or less similar to already existing tests. At that point,
the test does not really test the limits of possibly huge projects with
thousands of watched files, so it has been removed instead.
@Arnesfield
Copy link
Contributor Author

Looks like the test for 1000 watched files is failing due to a timeout error (tests have a timeout of 10000). I tried reducing it to 100 files and while it seems to pass, it does not really test the limits of possibly huge projects with thousands of watched files, so it has been removed instead.

Though, the test for watched files outside the current working directory is kept as it is one of the main behaviors that we probably would want to keep in check.

@mark-wiemer
Copy link
Member

mark-wiemer commented Jul 12, 2025

Going to stress test this one as we saw failures in 5 out of 30 runs on main branch with our --watch tests, ref #5361.

Ran 20 test runs on my branch

5/20 runs failed:

Next steps:

  • Learn more about Chokidar code and Mocha test architecture
  • Try to reproduce these issues and fixes locally
  • Review this PR in more depth (I read it but will need to read it a few more times given the significance of the changes)

@mark-wiemer mark-wiemer mentioned this pull request Jul 12, 2025
3 tasks
Copy link

@jedwards1211 jedwards1211 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should reduce unnecessary CPU and Memory usage in a few spots since the number of files X number of watch paths may get large.

*/
function cache(map, key, value) {
if (map.size >= MAX_CACHE_SIZE) {
map.clear();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why clear the map instead of just removing the first key from the keys() iterator here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The change would be something like:

// also make sure to update the doc comment for this function
// ...
if (map.size >= MAX_CACHE_SIZE && !map.has(key)) {
  map.delete(map.keys().next().value);
}
// ...

Where !map.has(key) makes sure to skip the delete if the key already exists (which wouldn't increase the map size).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had similar thoughts but didn't want to prematurely optimize :) if this has the same perf I'm fine with it, still very clean code :)

* @type {typeof Pattern[]} The `Pattern` class is not exported by the `glob` package.
* Ref [link](../../node_modules/glob/dist/commonjs/pattern.d.ts)
*/
const patterns = globPaths.reduce((acc, globPath) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there were comments about this, but .flatMap is a better way to do this

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n may not be large here, but reduce-concat is O(n^2) whereas flatmap is just O(n)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a for loop previously (5514fe2, iterating globPaths). We probably don't even need the array even if it's only a small performance cost for a slight readability improvement. Still, I'd prefer the for loop over an extra array (with .flatMap()/.reduce()) despite the additional nesting. Thoughts on reverting this change or going for flatMap @mark-wiemer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If flatMap works without introducing new dependencies let's go for it. If it's a whole new dependency just for this optimization, I'd have to see some numbers when this would have any significant impact. Given it's a simple iteration over patterns (not even matched files) I can't see it saving more than 1ms per run. That said, I do like clean code, and if flatMap is simpler / easier to maintain I'm happy to consider it. I'd just have to see them side-by-side :)

}
}

return Array.from(pattern.globs).some(globPath =>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will be run on potentially lots of files, it would be better to just iterate the set looking for a glob that matches instead of creating an array every time we test a path

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. We can revert this back to the previous for loop (caca650), but I'll let @mark-wiemer decide what we'd want to do.

Thanks for the feedback @jedwards1211.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I think my change here hurts performance, and since this is the number of files it could be significant. Feel free to revert if it improves perf and is still readable, maybe adding a comment like "use raw data to save time instead of copying into array"

@jedwards1211
Copy link

jedwards1211 commented Aug 16, 2025

I think we could afford to make those match caches bigger, too, like up to 10000

@jedwards1211
Copy link

jedwards1211 commented Aug 16, 2025

@mark-wiemer

Perf is definitely important here but I focused on readability first, hence the change to reduce and some

I don't see how reduce-concat is so much more readable than a nested for loop (or especially flatMap?) to justify its O(n^2) cost (where n is the size of the final array). It's a good pattern in FP languages based on linked lists where it's O(n); not such a good pattern in languages like JS that aren't based on linked lists.

Array.from(set).some(...) is more readable but it creates a array from the set for every call, which is a needless waste for a slight increase in readability (a for loop isn't that much harder to read).

@mark-wiemer
Copy link
Member

@mark-wiemer

Perf is definitely important here but I focused on readability first, hence the change to reduce and some

I don't see how reduce-concat is so much more readable than a nested for loop (or especially flatMap?) to justify its O(n^2) cost (where n is the size of the final array). It's a good pattern in FP languages based on linked lists where it's O(n); not such a good pattern in languages like JS that aren't based on linked lists.

Array.from(set).some(...) is more readable but it creates a array from the set for every call, which is a needless waste for a slight increase in readability (a for loop isn't that much harder to read).

Yeah, I may have been over-valuing readability and prioritizing my preferences as "objective." Happy to revert some of my changes, see other comments for details :)

- Use array `flatMap` instead of `reduce` for glob path patterns
- Revert glob path matching check to a `for` loop (commit caca650)
- Update `MAX_CACHE_SIZE` from 1,000 to 10,000
- Update map cache to only delete the first key whenever the limit is reached
- Update debug logging and formatting
@Arnesfield
Copy link
Contributor Author

Pushed up changes to address the recent comments. Let me know if I missed anything, thanks.

Some notes:

  • Opted for array flatMap over the for loop to replace the reduce call.
  • Removed JSON.stringify(entry) since entry can be a symbol (for globstar) and JSON.stringify(symbol) returns undefined.
  • I would have preferred const MAX_CACHE_SIZE = 10_000; but it seems to fail during lint since it's using ES2020 (numeric separators seem to be a feature under ES2021).

minimatch(filePath, globPath, {dot: true, windowsPathsNoEscape: true})
);
// loop through the set of glob paths instead of converting it into an array
for (const globPath of pattern.globs) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (const globPath of pattern.globs) {
// provides minor perf increase
for (const globPath of pattern.globs) {

@dylan-chong
Copy link

any progress on getting this merged? for the last 3 months i've been using mocha 11.1.0 just so i can use the watch feature...

i know youre risking a breaking change to a core feature, but it's already broken...

@mark-wiemer
Copy link
Member

any progress on getting this merged? for the last 3 months i've been using mocha 11.1.0 just so i can use the watch feature...

i know youre risking a breaking change to a core feature, but it's already broken...

I'll work with @JoshuaKGoldberg on this one. I would like to add it to v12 personally but I haven't looked at this in the past couple weeks and don't have a ton of expertise with glob matching, so I'm not confident I'd be able to identify subtle bugs here.

@mark-wiemer mark-wiemer changed the title fix: watch mode using chokidar v4 (#5355) fix: watch mode using chokidar v4 Sep 7, 2025
@dylan-chong
Copy link

Hey guys, any progress here?

This watch is super important to my workflow, without it, life is so much slower!
(Also my old mocha 11.1.0 stopped working due to some ESM/CJS shenans so I can't use that anymore nor do I want to try fix that)

The solution doesn't have to work perfectly. You'll have a chance to iron out the kinks if bug reports come in, but right now the watch mode is just a dead feature on most projects.

@JoshuaKGoldberg
Copy link
Member

Good timing: Mark and I are now both more free than we've been in a while, and focusing heavily on getting Mocha 12 through the door (#5357). It'd be nice to have this important fix in 11 - so it's top of my queue now.

Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK! I tested this to confirm it fixes the linked issues' reproductions:

There is a good bit of added logic in watch-run.js, and eventually we could (should?) probably extract it out to some shared helper, or even standalone package. But for now I think this is more than good enough. The comments are very helpful in understanding it.

Thanks for your hard work and patience with our reviews on this @Arnesfield! 🤎

* @typedef {import('chokidar').FSWatcher} FSWatcher
* @typedef {import('glob').Glob['patterns'][number]} Pattern
* The `Pattern` class is not exported by the `glob` package.
* Ref [link](../../node_modules/glob/dist/commonjs/pattern.d.ts).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we file an issue on glob to export it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They probably consider this internal API. I think we should ask them to export a public API like Glob.test(file: string): boolean that we can use instead of copying so much of its internal logic into createPathFilter here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I filed this issue, let's see what they say: isaacs/node-glob#632

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment here talks about exporting the Pattern class, but the issue talks about exporting a glob.test function, I think there was a disconnect here. Regardless, not a blocker to this PR :)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was saying above, the Pattern class seems internal (I doubt they would be willing to export it). You're welcome to ask them if they'll export it though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I posted a comment on the issue (isaacs/node-glob#632 (comment)) and it looks like the logic that uses the Pattern class for path parsing could be improved by using minimatch directly. Not sure when I'll have time to refactor it (I'll probably create a separate issue and PR), but I think for now the current implementation should still work as intended.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to consider: the logic copied from glob for dealing with patterns is a bit nontrivial, and glob has hopefully tested all possible cases that can happen with it, but we probably aren't testing them all here.

This is part of why I think the logic deserves to live in a reusable package.

If I have time soon I may make a proper package for working with multiple patterns, that puts them into a tree for more efficient matching and gives us a getTopDirs method, and has thorough unit tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made some changes to the path parsing logic to use Minimatch instead of Glob Pattern and while the tests seem to pass (after handling only a few of the differences in behavior), I came to the conclusion that using Glob Patterns is better as it produces more accurate path segments, especially for Windows paths.

It adds slashes for drives (e.g., C: to C:/). Getting the path segments of C:\Users\user for example:

  • Minimatch path segments: [ 'C:', 'Users', 'user' ]
  • Glob Pattern path segments: [ 'C:/', 'Users', 'user' ]

Small note: Glob uses slashes / even for Windows.

path.resolve() results:

// via NodeJS Windows

// using path segments from minimatch
console.log(path.resolve("C:\\a\\b\\c", "C:", "Users", "user"));
// output: C:\a\b\c\Users\user

// using path segments from glob pattern (more accurate)
console.log(path.resolve("C:\\a\\b\\c", "C:/", "Users", "user"));
// output: C:\Users\user

UNC paths also produce different path segments. Using \\host\share\a\b\c for example:

  • Minimatch path segments: [ '', '', 'host', 'share', 'a', 'b', 'c' ]
  • Glob Pattern path segments: [ '//host/share/', 'a', 'b', 'c' ]

Glob Pattern treats //host/share/ as one segment. path.resolve() seems to handle this case as well:

// via NodeJS Windows

// using path segments from minimatch
console.log(path.resolve('C:', '', '', 'host', 'share', 'a', 'b', 'c'));
// output: C:\host\share\a\b\c

// using path segments from glob pattern (more accurate)
console.log(path.resolve('C:', '//host/share/', 'a', 'b', 'c'));
// output: \\host\share\a\b\c

Refactoring to Minimatch as I previously suggested isn't the right call. Using UNC paths as watch paths probably won't make sense, but I guess the option is there so long as Chokidar is capable of watching those paths.

There may be more cases that Glob Pattern handles that I haven't looked into. In my opinion, we can keep the current way Pattern is referenced. It's hard to say if Pattern needs to be exported from the Glob package since we really only need a reference to the type and not the class itself just for explicitly typing a few variables.

@mark-wiemer
Copy link
Member

mark-wiemer commented Oct 1, 2025

@JoshuaKGoldberg still LGTM, I fixed the lint in #5475 and we'll work on test failures in #5361 of course. Ready to merge when you are :) just not sure if we're including this in v11 or if it'll be the first PR of v12

@JoshuaKGoldberg
Copy link
Member

Let's ship now so v11 users can benefit from the important bug fix. 🚢 !

@JoshuaKGoldberg JoshuaKGoldberg merged commit c2667c3 into mochajs:main Oct 1, 2025
86 of 92 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-major implementation requires increase of "major" version number; "breaking changes"

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Bug: Not able to watch files with huge node_modules anymore after bumping chokidar to v4 🐛 Bug: watching files outside of CWD is broken

5 participants