Skip to content

metro-file-map: Watch directories before listing them in fallback (Linux + Windows) watcher - #1907

Open
robhogan wants to merge 1 commit into
mainfrom
pr1907
Open

metro-file-map: Watch directories before listing them in fallback (Linux + Windows) watcher#1907
robhogan wants to merge 1 commit into
mainfrom
pr1907

Conversation

@robhogan

@robhogan robhogan commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary:
FallbackWatcher starts an fs.watch on each directory from recReaddir's dirCallback, and the crawl calls that after readdir has returned.

Anything written into a directory between the two appears in neither the listing nor the watch, so it stays invisible until the next full crawl. There's no recovery on Linux or Windows.

This is expo/expo#48950 - npx expo install against a running dev server leaves the new modules unresolvable until restart, which is a lot more painful than it sounds now that agents routinely run Metro in a VM. @brentvatne diagnosed it in expo/expo#49363, with a fix in Expo's fork that we can hopefully replace with this one.

Now that we own the crawl (#1906), the fix is very simply to call dirCallback before readdir instead of after, which also moves it inside recReaddir's existing try.

Separate issues not fixed here, both pre-existing and both covered by expo/expo#49363 :

  • An fs.watch that emits error is never removed from #watched. Node emits no close after error, so the path can never be re-watched, and #stopWatching waits on a close that will not arrive.
  • On win32, fs.watch can report an event with no filename. #detectChangedFile drops it when #dirRegistry[dir] is empty, which is exactly the state a newly watched directory is in.

Changelog:

 - **[Fix]**: `FallbackWatcher` no longer misses files written to a directory while it is being crawled

Test Plan:

yarn jest packages/metro-file-map
yarn flow check
yarn lint

New watchers/__tests__/FallbackWatcher-test.js asserts the ordering directly, that fs.watch precedes readdir for the same directory, on the initial crawl and on a directory created while watching, plus that a directory whose fs.watch throws is skipped without failing the crawl. All three fail on the parent commit and pass here.

The race can't be asserted behaviourally on macOS, because FSEvents delivers with a latency window and a watch started immediately after a write still reports it:

$ node -e "fs.writeFileSync(d+'/raced.js', ''); fs.watch(d, (e, f) => console.log(e, f))"
rename raced.js

That's why the issue is Linux/Windows only, and why the assertion is on the ordering rather than on a missed event. Metro has no Windows coverage for this backend, so the win32 path is unexercised either way.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 7, 2026
@robhogan robhogan changed the title metro-file-map: Watch directories before listing them metro-file-map: Watch directories before listing them, fix dropped files on (eg) npm installs Sep 7, 2026
@robhogan

robhogan commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

NB: This is stacked on #1906. I assume shipit doesn't support stacks (?), but this should automatically rebase against main and be importable once D119076649 lands.

@robhogan robhogan changed the title metro-file-map: Watch directories before listing them, fix dropped files on (eg) npm installs metro-file-map: Watch directories before listing them Sep 7, 2026
@robhogan
robhogan changed the base branch from pr1906 to main September 7, 2026 15:06
Summary:
`FallbackWatcher` starts an `fs.watch` on each directory from `recReaddir`'s `dirCallback`, and the crawl calls that after `readdir` has returned.

Anything written into a directory *between the two* appears in neither the listing nor the watch, so it stays invisible until the next full crawl. There's no recovery on Linux or Windows.

This is expo/expo#48950 - `npx expo install` against a running dev server leaves the new modules unresolvable until restart, which is a lot more painful than it sounds now that agents routinely run Metro in a VM. @brentvatne diagnosed it in expo/expo#49363, with a fix in Expo's fork that we can hopefully replace with this one.

Now that we own the crawl (#1906), the fix is very simply to call `dirCallback` before `readdir` instead of after, which also moves it inside `recReaddir`'s existing `try`.

Separate issues not fixed here, both pre-existing and both covered by expo/expo#49363 :

- An `fs.watch` that emits `error` is never removed from `#watched`. Node emits no `close` after `error`, so the path can never be re-watched, and `#stopWatching` waits on a `close` that will not arrive.
- On win32, `fs.watch` can report an event with no filename. `#detectChangedFile` drops it when `#dirRegistry[dir]` is empty, which is exactly the state a newly watched directory is in.

Changelog:
```
 - **[Fix]**: `FallbackWatcher` no longer misses files written to a directory while it is being crawled
```

Test Plan:
```
yarn jest packages/metro-file-map
yarn flow check
yarn lint
```

New `watchers/__tests__/FallbackWatcher-test.js` asserts the ordering directly, that `fs.watch` precedes `readdir` for the same directory, on the initial crawl and on a directory created while watching, plus that a directory whose `fs.watch` throws is skipped without failing the crawl. All three fail on the parent commit and pass here.

The race can't be asserted behaviourally on macOS, because FSEvents delivers with a latency window and a watch started immediately after a write still reports it:

```
$ node -e "fs.writeFileSync(d+'/raced.js', ''); fs.watch(d, (e, f) => console.log(e, f))"
rename raced.js
```

That's why the issue is Linux/Windows only, and why the assertion is on the ordering rather than on a missed event. Metro has no Windows coverage for this backend, so the win32 path is unexercised either way.
@robhogan

robhogan commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

(Rebasing wasn't automatic because GH sees a ship-it merged PR as closed and assumes the stack is broken. Anyway, did it the old-fashioned way)

@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 7, 2026
@robhogan robhogan changed the title metro-file-map: Watch directories before listing them metro-file-map: Watch directories before listing them in fallback (Linux + Windows) watcher Sep 7, 2026
@robhogan

robhogan commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

@javache / @GijsWeterings / @vzaidman this is quite a high impact fix, it turns out. Metro on Linux outside Meta (eg, agentic dev) gets in a real mess during package installs without this.

Meta doesn't use FallbackWatcher so there should be no risk there.

Thanks 馃檹

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant