Use extension handle to identify extensions in file-watcher#7224
Use extension handle to identify extensions in file-watcher#7224isaacroldan wants to merge 1 commit intomainfrom
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Pull request overview
This PR refactors the dev file-watcher event model to attribute file changes to extensions by extension handle rather than only by extension directory path, enabling correct handling of shared files across multiple extensions.
Changes:
- Adds
extensionHandle?: stringtoWatcherEventand threads it through event creation/deduping. - Changes watched-file ownership tracking to map file paths → extension handles.
- Updates watcher event handling logic (and tests) to support multiple events for shared files.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/app/src/cli/services/dev/app-events/file-watcher.ts | Emits watcher events with extensionHandle and tracks watched files by handle. |
| packages/app/src/cli/services/dev/app-events/file-watcher.test.ts | Adjusts tests to allow multiple emitted events for a single FS change. |
| packages/app/src/cli/services/dev/app-events/app-event-watcher-handler.ts | Resolves affected extensions by extensionHandle when present, otherwise by directory. |
Comments suppressed due to low confidence (1)
packages/app/src/cli/services/dev/app-events/file-watcher.test.ts:383
- When
expectedEventCountis 2, the test still only asserts againstactualEvents[0]. Since the watcher can emit multiple events in either order (and the whole point of this PR is to differentiate them viaextensionHandle), this can become order-dependent/flaky and it also doesn't validate thatextensionHandleis set correctly. Prefer asserting thatactualEventscontains an event matchingexpectedEvent(and, when multiple events are expected, assert the full set ofextensionHandlevalues or the full list of expected events) rather than assuming index 0.
const calls = onChange.mock.calls
const actualEvents = calls.find((call) => call[0].length > 0)?.[0]
if (!actualEvents) {
throw new Error('Expected onChange to be called with events, but all calls had empty arrays')
}
const eventCount = expectedEventCount ?? 1
expect(actualEvents).toHaveLength(eventCount)
const actualEvent = actualEvents[0]
expect(actualEvent.type).toBe(expectedEvent.type)
expect(actualEvent.path).toBe(normalizePath(expectedEvent.path))
expect(actualEvent.extensionPath).toBe(normalizePath(expectedEvent.extensionPath))
expect(Array.isArray(actualEvent.startTime)).toBe(true)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const extensionResults = this.app.realExtensions.map((extension) => ({ | ||
| extension, | ||
| watchedFiles: extension.watchedFiles(), | ||
| })) | ||
|
|
||
| const allFiles = new Set<string>() | ||
| for (const {extension, watchedFiles} of extensionResults) { | ||
| const extensionDir = normalizePath(extension.directory) | ||
| for (const file of watchedFiles) { | ||
| const normalizedPath = normalizePath(file) | ||
| allFiles.add(normalizedPath) | ||
|
|
||
| // Track which extensions watch this file | ||
| const extensionsSet = this.extensionWatchedFiles.get(normalizedPath) ?? new Set() | ||
| extensionsSet.add(extensionDir) | ||
| this.extensionWatchedFiles.set(normalizedPath, extensionsSet) | ||
| // Track which extension handles watch this file | ||
| const handlesSet = this.extensionWatchedFiles.get(normalizedPath) ?? new Set() | ||
| handlesSet.add(extension.handle) | ||
| this.extensionWatchedFiles.set(normalizedPath, handlesSet) | ||
| } |
There was a problem hiding this comment.
getAllWatchedFiles() now iterates over app.realExtensions, which includes app config (configuration-experience) extensions whose directory is typically the app root (./). Since ExtensionInstance.watchedFiles() defaults to **/* when no custom watch paths are defined, this can cause the watcher to add (and attribute events to) a very large set of app-root files for config extensions, which looks like a performance regression and can produce incorrect extension update events. Consider reverting to app.nonConfigExtensions (or explicitly filtering out ext.isAppConfigExtension) when building the watched-file map so only real extension directories contribute watched files.
| const extension = event.extensionHandle | ||
| ? this.app.realExtensions.find((ext) => ext.handle === event.extensionHandle) | ||
| : undefined | ||
| const watchPaths = extension?.watchedFiles() | ||
| const ignoreInstance = this.ignored[event.extensionPath] | ||
| const ignoreInstance = extension ? this.ignored[extension.directory] : undefined | ||
|
|
||
| if (watchPaths) { | ||
| const isAValidWatchedPath = watchPaths.some((pattern) => matchGlob(event.path, pattern)) | ||
| return !isAValidWatchedPath | ||
| } else if (ignoreInstance) { | ||
| const relative = relativePath(event.extensionPath, event.path) | ||
| const relative = relativePath(extension!.directory, event.path) | ||
| return ignoreInstance.ignores(relative) |
There was a problem hiding this comment.
updateApp() stores ignore instances keyed by normalizePath(ext.directory), but shouldIgnoreEvent() now looks them up via this.ignored[extension.directory] (non-normalized). On Windows (or any mixed-separator input), this key mismatch can prevent .gitignore-based ignoring from being applied. Use a consistently-normalized key (e.g., normalizePath(extension.directory) or event.extensionPath) for the ignored map lookup, and likewise use the same normalized base when computing the relativePath.
b5640a9 to
8053877
Compare
Co-authored-by: Claude Code <claude-code@anthropic.com>
8053877 to
cc5abaa
Compare

WHY are these changes introduced?
The file watcher was using extension directory paths to identify which extensions are affected by file changes. This approach has limitations when dealing with shared files or when extensions need to be uniquely identified beyond their filesystem location.
WHAT is this pull request doing?
Refactors the file watcher system to use extension handles instead of directory paths for tracking file ownership and identifying affected extensions. The changes include:
extensionHandlefield toWatcherEventinterface to uniquely identify extensionsThe
extensionPathfield is retained for backward compatibility and folder-level events where no extension handle exists yet.How to test your changes?
Checklist
pnpm changeset add