Skip to content

Fix case sensitivity fswatch and users - #64210

Queued
Jake Bailey (jakebailey) wants to merge 2 commits into
microsoft:mainfrom
jakebailey:watch-invalidation-refactor
Queued

Jake Bailey (jakebailey) wants to merge 2 commits into
microsoft:mainfrom
jakebailey:watch-invalidation-refactor

Conversation

@jakebailey

@jakebailey Jake Bailey (jakebailey) commented Sep 9, 2026

Copy link
Copy Markdown
Member

Fixes #64089

This has two parts:

  • fsevents is unlike other watchers. On Linux, Windows, etc, you register a watch at a location and then get updates relative to that. But fsevents does callbacks with paths of its own making, which can differ from the path names you originally watched. This causes mismatches where we get an event and it doesn't match anything we registered. So, we need to apply macOS's file path comparison to check the paths it sends us to line them back up with our own watches, then re-root them properly.
  • The DirWatchSet stored paths as canonical, but then used those for watching, which caused more mismatches. Store both forms.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad changes to native watcher routing, snapshot synchronization, realpath caching, and incremental builds require final human validation, especially on macOS.

Pull request overview

Extends macOS watch handling so differently cased or normalized filesystem paths are routed back to compiler-visible names without changing compiler identity.

Changes:

  • Adds native Darwin path comparison and case-aware watch routing.
  • Introduces watch-alias indexing across project, CLI, and build watches.
  • Adds realpath caching support and broad regression/performance coverage.
File summaries
File Description
tsc/testdata/baselines/reference/tsbuildWatch/dependencyUpdate/watches-absolute-non-root-dependency-updates.js Updates preserved watch-path casing.
tsc/internal/watchalias/registration.go Implements alias registration and event matching.
tsc/internal/watchalias/index.go Adds the watch-alias index.
tsc/internal/watchalias/index_test.go Tests alias and realpath behavior.
tsc/internal/watchalias/index_darwin_test.go Tests native Darwin aliases.
tsc/internal/watchalias/capability_test.go Tests capability propagation.
tsc/internal/vfs/vfs.go Adds parent-assisted realpath resolution.
tsc/internal/vfs/vfs_test.go Tests realpath fallback behavior.
tsc/internal/vfs/osvfs/realpath_darwin.go Optimizes Darwin realpath resolution.
tsc/internal/vfs/osvfs/realpath_darwin_test.go Tests Darwin realpath semantics.
tsc/internal/vfs/osvfs/os.go Exposes native watch comparison.
tsc/internal/vfs/cachedvfs/cachedvfs.go Propagates new VFS capabilities.
tsc/internal/project/watchnotifications.go Prepares and expands watch notifications.
tsc/internal/project/watchalias.go Integrates aliases into snapshots.
tsc/internal/project/watchalias_darwin_test.go Adds end-to-end Darwin watch tests.
tsc/internal/project/watchalias_coalescing_darwin_test.go Tests native lifecycle coalescing.
tsc/internal/project/watch.go Preserves original watch spellings.
tsc/internal/project/snapshotfs.go Tracks realpath observations.
tsc/internal/project/snapshot.go Refreshes aliases during cloning.
tsc/internal/project/session.go Serializes watch preparation and publication.
tsc/internal/project/session_test.go Updates expected watch casing.
tsc/internal/project/projectcollectionbuilder.go Reuses missing-directory tracking.
tsc/internal/project/project.go Stores original watched filenames.
tsc/internal/project/overlayfs.go Coalesces compiler-equivalent notifications.
tsc/internal/project/filechange.go Retains filesystem-change metadata.
tsc/internal/project/configfileregistrybuilder.go Supports full config invalidation.
tsc/internal/project/configfileregistry.go Retains config lookup spelling.
tsc/internal/project/api.go Expands API-provided watch summaries.
tsc/internal/fswatch/watcher.go Adds comparer-aware routing and filtering.
tsc/internal/fswatch/watcher_test.go Tests comparer and callback behavior.
tsc/internal/fswatch/README.md Documents macOS path semantics.
tsc/internal/fswatch/pathkey.go Exposes immutable path comparers.
tsc/internal/fswatch/pathcompare.go Implements path comparison and rebasing.
tsc/internal/fswatch/fsevents_darwin.go Applies native comparison to FSEvents.
tsc/internal/fswatch/fsevents_darwin_shared_test.go Tests FSEvents routing and performance.
tsc/internal/fswatch/fsevents_darwin_nfd_test.go Tests Darwin Unicode aliases.
tsc/internal/fswatch/fsevents_darwin_ffi.s Adds the CFStringFold trampoline.
tsc/internal/fswatch/fsevents_darwin_ffi.go Implements CoreFoundation folding.
tsc/internal/fswatch/CHANGES.md Records macOS comparison behavior.
tsc/internal/fswatch/canonicalize_other.go Defines exact non-Darwin comparison.
tsc/internal/fswatch/canonicalize_darwin.go Queries Darwin volume sensitivity.
tsc/internal/execute/watchmanager/watchmanager.go Adds alias and resolution lifecycle management.
tsc/internal/execute/watchmanager/watchmanager_test.go Tests watch generations and deletions.
tsc/internal/execute/watchmanager/watchalias_darwin_test.go Tests native alias resolution.
tsc/internal/execute/watcher.go Integrates aliases into CLI watch mode.
tsc/internal/execute/tsctests/contentmapper_watch_test.go Tests manifest casing changes.
tsc/internal/execute/build/watchchanges_test.go Tests ancestor deletion matching.
tsc/internal/execute/build/orchestrator.go Integrates aliases into build watch mode.
tsc/internal/execute/build/compilerHost.go Tracks build filesystem reads.
tsc/internal/execute/build/buildtask.go Retains watched build dependencies.
tsc/internal/bundled/embed.go Propagates VFS watch capabilities.
Review details
  • Files reviewed: 55/55 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@jakebailey

Copy link
Copy Markdown
Member Author

This is nasty, I'm going to try and simplify it, but I think it can only be simpler by doing less precise tracking...

@jakebailey

Copy link
Copy Markdown
Member Author

It doesn't save much code to simplify it, with much worse downsides, sadly.

@jakebailey

Copy link
Copy Markdown
Member Author

The FS overlay stuff very much conflicted, so, I have to figure that out

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Symlinked logical directory roots are not registered as directories, so descendant creation events can still be missed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Comment thread tsc/internal/execute/watchmanager/watchmanager.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

The large cross-cutting watch, filesystem, snapshot, and build changes include platform-specific behavior that requires final human validation on macOS.

Review effort: Balanced
Findings: None

Resolved since last review (1)

Case-insensitive deduplication must not change the name passed to the
filesystem watcher. FSEvents reports the on-disk spelling, so replacing
Mixed with mixed can make an otherwise ordinary project miss events.

Keep canonical keys for coverage checks and the first observed spelling
for subscriptions. Update the watch-set regression and build baseline
to distinguish these two responsibilities.
A valid path can differ from the name returned by FSEvents in casing or
Unicode normalization, including folds that change length. Exact prefix
and WatchFile comparisons silently lose those notifications.

Use volume-aware native comparison for FSEvents and kqueue, rebasing
matches to the subscriber's root without changing child identities.
Case-sensitive volumes and unrelated siblings remain distinct. Cache
prepared roots and fold incoming paths lazily so shared streams do not
repeat Unicode work for every subscription.

The native routing, normalization, overflow, and file-watch regressions
belong with this fix; compiler invalidation is handled separately.
@jakebailey

Copy link
Copy Markdown
Member Author

I'm currently splitting this PR into a few, since this actually fixes three marginally related things:


// PathComparerForPath returns exact comparison on platforms without native
// Darwin watch aliases. It does not inspect the host filesystem.
func PathComparerForPath(path string) (PathComparer, error) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This API has leaked in from future changes; it's probably not harmful to leave.

@jakebailey

Copy link
Copy Markdown
Member Author

I'm going to not bother with 3 as I don't think any normal client would do that, but for 4-8, that depends on this PR, so I'll send that later.

@jakebailey
Jake Bailey (jakebailey) added this pull request to the merge queue Sep 22, 2026

This branch has not been deployed

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

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FSEvents watcher drops events when requested casing differs from disk casing

3 participants