From 908eeccb69e51c7a909dc19f9600eb347e5a6f88 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:07:56 -0500 Subject: [PATCH 1/5] reload cached files for created watch events --- tsc/internal/project/bulkcache_test.go | 37 +++++ tsc/internal/project/overlayfs.go | 40 +++--- tsc/internal/project/overlayfs_test.go | 49 +++++++ tsc/internal/project/session_test.go | 103 ++++++++++++++ tsc/internal/project/snapshot.go | 3 +- tsc/internal/project/snapshotfs.go | 149 +++++++++++++------- tsc/internal/project/snapshotfs_test.go | 180 +++++++++++++++++++++++- 7 files changed, 486 insertions(+), 75 deletions(-) diff --git a/tsc/internal/project/bulkcache_test.go b/tsc/internal/project/bulkcache_test.go index 19451c6473fe1..ff7e89a9de2c1 100644 --- a/tsc/internal/project/bulkcache_test.go +++ b/tsc/internal/project/bulkcache_test.go @@ -116,6 +116,43 @@ func TestBulkCacheInvalidation(t *testing.T) { }) }) + t.Run("large all-created batch reloads cached file", func(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + eventCount int + eventURI lsproto.DocumentUri + }{ + {name: "single parent directory", eventURI: "file:///project/src"}, + {name: "excessive exact file", eventCount: 1001, eventURI: "file:///project/src/helper.ts"}, + {name: "excessive parent directory", eventCount: 1001, eventURI: "file:///project/src"}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + session, utils := projecttestutil.Setup(baseFiles) + + session.DidOpenFile(context.Background(), "file:///project/src/index.ts", 1, baseFiles["/project/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + ls, err := session.GetLanguageService(context.Background(), "file:///project/src/index.ts") + assert.NilError(t, err) + assert.Equal(t, ls.GetProgram().GetSourceFile("/project/src/helper.ts").Text(), `export const helper = "test";`) + + err = utils.FS().WriteFile("/project/src/helper.ts", `export const helper = "updated";`) + assert.NilError(t, err) + + fileEvents := generateFileEvents(test.eventCount, "file:///project/generated/file%d.ts", lsproto.FileChangeTypeCreated) + fileEvents = append(fileEvents, &lsproto.FileEvent{ + Uri: test.eventURI, + Type: lsproto.FileChangeTypeCreated, + }) + session.DidChangeWatchedFiles(context.Background(), fileEvents) + + ls, err = session.GetLanguageService(context.Background(), "file:///project/src/index.ts") + assert.NilError(t, err) + assert.Equal(t, ls.GetProgram().GetSourceFile("/project/src/helper.ts").Text(), `export const helper = "updated";`) + }) + } + }) + t.Run("large number of changes outside node_modules", func(t *testing.T) { t.Parallel() test := func(t *testing.T, fileEvents []*lsproto.FileEvent, expectConfigReload bool) { diff --git a/tsc/internal/project/overlayfs.go b/tsc/internal/project/overlayfs.go index 458b94d0ee831..36c9302be5267 100644 --- a/tsc/internal/project/overlayfs.go +++ b/tsc/internal/project/overlayfs.go @@ -417,13 +417,14 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma // Reduced collection of changes that occurred on a single file type fileEvents struct { - openChange *FileChange - closeChange *FileChange - watchChanged bool - changes []*FileChange - saved bool - created bool - deleted bool + openChange *FileChange + closeChange *FileChange + watchChanged bool + changes []*FileChange + saved bool + created bool + deleted bool + watchObserved bool } fileEventMap := make(map[lsproto.DocumentUri]*fileEvents) @@ -439,6 +440,7 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma events = &fileEvents{} fileEventMap[uri] = events } + events.watchObserved = events.watchObserved || change.Kind.IsWatchKind() if !result.IncludesWatchChangeOutsideNodeModules && change.Kind.IsWatchKind() && !strings.Contains(string(uri), "/node_modules/") { result.IncludesWatchChangeOutsideNodeModules = true @@ -529,16 +531,8 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma o = nil } - if events.watchChanged { - if o == nil { - result.Changed.Add(uri) - } else if o != nil && !events.saved { - if matchesDiskText, _ := o.computeMatchesDiskText(fs.host); matchesDiskText != o.MatchesDiskText() { - o = newOverlay(o.FileName(), o.Content(), o.Version(), o.kind) - o.matchesDiskText = matchesDiskText - newOverlays[path] = o - } - } + if events.watchChanged && o == nil { + result.Changed.Add(uri) } if len(events.changes) > 0 && o != nil { @@ -567,15 +561,15 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma } } - if events.saved { - if o != nil { + if o != nil && (events.watchObserved || events.saved) { + if matchesDiskText, _ := o.computeMatchesDiskText(fs.host); matchesDiskText != o.MatchesDiskText() { o = newOverlay(o.FileName(), o.Content(), o.Version(), o.kind) - o.matchesDiskText = true + o.matchesDiskText = matchesDiskText newOverlays[path] = o - } else if !events.watchChanged { - // File was saved but never opened via didOpen; treat as a disk change. - result.Changed.Add(uri) } + } else if events.saved && o == nil && !events.watchChanged { + // File was saved but never opened via didOpen; treat as a disk change. + result.Changed.Add(uri) } if events.created && o == nil { diff --git a/tsc/internal/project/overlayfs_test.go b/tsc/internal/project/overlayfs_test.go index 042764dda8267..26f1bcff3f339 100644 --- a/tsc/internal/project/overlayfs_test.go +++ b/tsc/internal/project/overlayfs_test.go @@ -146,6 +146,7 @@ func TestProcessChanges(t *testing.T) { }, }) // Then save + assert.NilError(t, fs.host.WriteFile(testURI1.FileName(), "const x = 1;")) result, _ := fs.processChanges([]FileChange{ { Kind: FileChangeKindSave, @@ -228,6 +229,7 @@ func TestProcessChanges(t *testing.T) { assert.Assert(t, !fs.GetFile(testURI1.FileName()).MatchesDiskText()) // Then save + assert.NilError(t, fs.host.WriteFile(testURI1.FileName(), "const x = 1;")) fs.processChanges([]FileChange{ { Kind: FileChangeKindSave, @@ -237,6 +239,7 @@ func TestProcessChanges(t *testing.T) { assert.Assert(t, fs.GetFile(testURI1.FileName()).MatchesDiskText()) // Now process a watch change + assert.NilError(t, fs.host.WriteFile(testURI1.FileName(), "const x = 2;")) fs.processChanges([]FileChange{ { Kind: FileChangeKindWatchChange, @@ -246,6 +249,52 @@ func TestProcessChanges(t *testing.T) { assert.Assert(t, !fs.GetFile(testURI1.FileName()).MatchesDiskText()) }) + t.Run("save and watch create compare overlay with disk in either order", func(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + changes []FileChange + }{ + { + name: "save then create", + changes: []FileChange{ + {Kind: FileChangeKindSave, URI: testURI1}, + {Kind: FileChangeKindWatchCreate, URI: testURI1}, + }, + }, + { + name: "create then save", + changes: []FileChange{ + {Kind: FileChangeKindWatchCreate, URI: testURI1}, + {Kind: FileChangeKindSave, URI: testURI1}, + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + fs := createOverlayFS() + const overlayContent = "const value = 'overlay';" + fs.processChanges([]FileChange{ + { + Kind: FileChangeKindOpen, + URI: testURI1, + Version: 1, + Content: overlayContent, + LanguageKind: lsproto.LanguageKindTypeScript, + }, + }) + assert.NilError(t, fs.host.WriteFile(testURI1.FileName(), "const value = 'disk';")) + + fs.processChanges(test.changes) + + file := fs.GetFile(testURI1.FileName()) + assert.Assert(t, file != nil) + assert.Equal(t, file.Content(), overlayContent) + assert.Assert(t, !file.MatchesDiskText()) + }) + } + }) + t.Run("save without overlay should not panic", func(t *testing.T) { t.Parallel() fs := createOverlayFS() diff --git a/tsc/internal/project/session_test.go b/tsc/internal/project/session_test.go index 140632689c940..81199e3731803 100644 --- a/tsc/internal/project/session_test.go +++ b/tsc/internal/project/session_test.go @@ -529,6 +529,41 @@ func TestSession(t *testing.T) { snapshot = session.Snapshot() assert.Equal(t, snapshot.GetFile("/home/projects/TS/p1/src/index.ts").MatchesDiskText(), true) }) + + t.Run("created watch event compares overlay with disk in either order", func(t *testing.T) { + t.Parallel() + for _, saveFirst := range []bool{true, false} { + name := "watch event first" + if saveFirst { + name = "save event first" + } + t.Run(name, func(t *testing.T) { + t.Parallel() + session, utils := projecttestutil.Setup(defaultFiles) + const uri = lsproto.DocumentUri("file:///home/projects/TS/p1/src/index.ts") + overlayContent := defaultFiles["/home/projects/TS/p1/src/index.ts"].(string) + session.DidOpenFile(context.Background(), uri, 1, overlayContent, lsproto.LanguageKindTypeScript) + assert.NilError(t, utils.FS().WriteFile(uri.FileName(), "const disk = true;")) + + watch := func() { + session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{Type: lsproto.FileChangeTypeCreated, Uri: uri}}) + } + if saveFirst { + session.DidSaveFile(context.Background(), uri) + watch() + } else { + watch() + session.DidSaveFile(context.Background(), uri) + } + + session.WaitForBackgroundTasks() + session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) + file := session.Snapshot().GetFile(uri.FileName()) + assert.Equal(t, file.Content(), overlayContent) + assert.Assert(t, !file.MatchesDiskText()) + }) + } + }) }) t.Run("Source file sharing", func(t *testing.T) { @@ -661,6 +696,74 @@ func TestSession(t *testing.T) { assert.Check(t, lsAfter.GetProgram() != programBefore) }) + t.Run("create closed program file reloads content (atomic save)", func(t *testing.T) { + t.Parallel() + files := maps.Clone(defaultFiles) + session, utils := projecttestutil.Setup(files) + + session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + + lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + assert.NilError(t, err) + programBefore := lsBefore.GetProgram() + + err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 2;`) + assert.NilError(t, err) + + // Simulate atomic save / rename where watcher reports Created (type 1) + session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeCreated, + Uri: "file:///home/projects/TS/p1/src/x.ts", + }, + }) + + lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + assert.NilError(t, err) + assert.Check(t, lsAfter.GetProgram() != programBefore) + srcX := lsAfter.GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") + assert.Assert(t, srcX != nil) + assert.Equal(t, srcX.Text(), "export const x = 2;") + }) + + t.Run("atomic save sequence with temp file reloads content", func(t *testing.T) { + t.Parallel() + files := maps.Clone(defaultFiles) + session, utils := projecttestutil.Setup(files) + + session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) + + lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + assert.NilError(t, err) + programBefore := lsBefore.GetProgram() + + err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 42;`) + assert.NilError(t, err) + + // Sequence sent by editors like VS Code on atomic save: tmp create -> target create -> tmp delete + session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeCreated, + Uri: "file:///home/projects/TS/p1/src/x.ts.tmp", + }, + { + Type: lsproto.FileChangeTypeCreated, + Uri: "file:///home/projects/TS/p1/src/x.ts", + }, + { + Type: lsproto.FileChangeTypeDeleted, + Uri: "file:///home/projects/TS/p1/src/x.ts.tmp", + }, + }) + + lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + assert.NilError(t, err) + assert.Check(t, lsAfter.GetProgram() != programBefore) + srcX := lsAfter.GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") + assert.Assert(t, srcX != nil) + assert.Equal(t, srcX.Text(), "export const x = 42;") + }) + t.Run("change program file not in tsconfig root files", func(t *testing.T) { t.Parallel() for _, workspaceDir := range []string{"/", "/home/projects/TS/p1", "/somewhere/else/entirely"} { diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index 4203b49cc47ad..f77681ca69b95 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -144,6 +144,8 @@ func (s *Snapshot) processFileChanges( } previousOpenFiles := overlayFileHandles(previousOverlays) openFiles := overlayFileHandles(overlays) + fileChanges = s.fs.expandRealpathAliases(fileChanges, fs.fs) + fileChanges = fs.expandCreatedCacheEntries(fileChanges) if fileChanges.HasExcessiveWatchEvents() { invalidateStart := time.Now() if fileChanges.InvalidateAll { @@ -178,7 +180,6 @@ func (s *Snapshot) processFileChanges( } _, contentMapperWatchedFiles := s.contentMapperWatchState() fileChanges = fs.expandAndFilterWatchEvents(fileChanges, contentMapperExtensions, contentMapperWatchedFiles, previousOpenFiles, openFiles) - fileChanges = s.fs.expandRealpathAliases(fileChanges) fileChanges = fs.markDirtyFiles(fileChanges) fileChanges = fs.convertOpenAndCloseToChanges(fileChanges, previousOpenFiles, openFiles) } diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index 3e95fb3709231..898afb553e278 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -464,31 +464,54 @@ func (s *snapshotFSBuilder) reloadEntryIfNeeded(entry *dirty.SyncMapEntry[tspath } func (s *snapshotFSBuilder) watchChangesOverlapCache(change FileChangeSummary, previousOpenFiles map[tspath.PathKey]FileHandle, openFiles map[tspath.PathKey]FileHandle) bool { - for uri := range change.Changed.Keys() { - path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) - if previousOpenFiles[path] != nil || openFiles[path] != nil { - return true - } - if _, ok := s.cacheFiles.Load(path); ok { - return true - } - if _, ok := s.nodeModulesRealpathAliases.Load(path); ok { - return true + for _, changes := range []collections.Set[lsproto.DocumentUri]{change.Changed, change.Created, change.Deleted} { + for uri := range changes.Keys() { + path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) + if previousOpenFiles[path] != nil || openFiles[path] != nil { + return true + } + if _, ok := s.cacheFiles.Load(path); ok { + return true + } + if _, ok := s.nodeModulesRealpathAliases.Load(path); ok { + return true + } } } - for uri := range change.Deleted.Keys() { - path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) - if previousOpenFiles[path] != nil || openFiles[path] != nil { - return true - } - if _, ok := s.cacheFiles.Load(path); ok { - return true - } - if _, ok := s.nodeModulesRealpathAliases.Load(path); ok { - return true + return false +} + +func (s *snapshotFSBuilder) expandCreatedCacheEntries(change FileChangeSummary) FileChangeSummary { + if change.Created.Len() == 0 { + return change + } + createdDirectories := collections.NewSetWithSizeHint[tspath.PathKey](change.Created.Len()) + for uri := range change.Created.Keys() { + if s.fs.DirectoryExists(tspath.RootedDirectoryPathFromPath(uri.FileName().AsPath())) { + createdDirectories.Add(s.caseSensitivity.PathKey(uri.FileName().AsPath())) } } - return false + if createdDirectories.Len() == 0 { + return change + } + s.cacheFiles.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *cachedFile]) bool { + path := entry.Key() + for { + if createdDirectories.Has(path) { + if file := entry.Value(); file != nil { + change.Created.Add(lsconv.FileNameToDocumentURI(file.FileName())) + } + break + } + parent := path.Parent() + if parent == path { + break + } + path = parent + } + return true + }) + return change } func (s *snapshotFSBuilder) invalidateCache() { @@ -512,12 +535,19 @@ func (s *snapshotFSBuilder) invalidateNodeModulesCache() { } func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) FileChangeSummary { - if change.Changed.Len() > 0 { + filterChanges := func(changes collections.Set[lsproto.DocumentUri], created bool) collections.Set[lsproto.DocumentUri] { + if changes.Len() == 0 { + return changes + } var filteredChanged collections.SyncSet[lsproto.DocumentUri] wg := core.NewWorkGroup(false) - for uri := range change.Changed.Keys() { + for uri := range changes.Keys() { path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) - if file := s.fs.GetFileByPath(uri.FileName(), path); file != nil && file.IsOverlay() { + if created { + if s.fs.Overlays()[path] != nil { + continue + } + } else if file := s.fs.GetFileByPath(uri.FileName(), path); file != nil && file.IsOverlay() { filteredChanged.Add(uri) continue } @@ -533,12 +563,16 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) FileChangeS }) } wg.RunAndWait() - newChanged := collections.NewSetWithSizeHint[lsproto.DocumentUri](filteredChanged.Size()) + newSet := collections.NewSetWithSizeHint[lsproto.DocumentUri](filteredChanged.Size()) for uri := range filteredChanged.Keys() { - newChanged.Add(uri) + newSet.Add(uri) } - change.Changed = *newChanged + return *newSet } + + change.Changed = filterChanges(change.Changed, false) + change.Created = filterChanges(change.Created, true) + for uri := range change.Deleted.Keys() { path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) if entry, ok := s.cacheFiles.Load(path); ok { @@ -582,39 +616,60 @@ func (s *snapshotFSBuilder) reloadEntryIfContentChanged(entry *dirty.SyncMapEntr return changed } -// expandRealpathAliases adds synthetic URIs to the Changed and Deleted sets for +// expandRealpathAliases adds synthetic URIs to the Changed, Created, and Deleted sets for // files that were accessed through node_modules symlinks. When a watch event arrives // using a realpath, this expands it to include the symlink-based path so that // downstream consumers (markDirtyFiles, markFilesChanged) can find cached entries. -func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary) FileChangeSummary { +func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary, currentFS LayeredFileSystem) FileChangeSummary { if len(s.nodeModulesRealpathAliases) == 0 { return change } - var additionalChanged collections.Set[lsproto.DocumentUri] - for uri := range change.Changed.Keys() { - path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) - if aliases, ok := s.nodeModulesRealpathAliases[path]; ok { - for _, aliasFileName := range aliases.paths { - additionalChanged.Add(lsconv.FileNameToDocumentURI(aliasFileName)) + expandSet := func(set *collections.Set[lsproto.DocumentUri]) { + if set.Len() == 0 { + return + } + var additional collections.Set[lsproto.DocumentUri] + for uri := range set.Keys() { + path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) + if aliases, ok := s.nodeModulesRealpathAliases[path]; ok { + for _, aliasFileName := range aliases.paths { + additional.Add(lsconv.FileNameToDocumentURI(aliasFileName)) + } } } - } - for uri := range additionalChanged.Keys() { - change.Changed.Add(uri) + for uri := range additional.Keys() { + set.Add(uri) + } } - var additionalDeleted collections.Set[lsproto.DocumentUri] - for uri := range change.Deleted.Keys() { - path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) - if aliases, ok := s.nodeModulesRealpathAliases[path]; ok { - for _, aliasFileName := range aliases.paths { - additionalDeleted.Add(lsconv.FileNameToDocumentURI(aliasFileName)) + expandSet(&change.Changed) + expandSet(&change.Created) + expandSet(&change.Deleted) + + if change.Created.Len() > 0 { + createdDirectories := collections.NewSetWithSizeHint[tspath.PathKey](change.Created.Len()) + for uri := range change.Created.Keys() { + if currentFS.DirectoryExists(tspath.RootedDirectoryPathFromPath(uri.FileName().AsPath())) { + createdDirectories.Add(s.caseSensitivity.PathKey(uri.FileName().AsPath())) + } + } + for realpath, aliases := range s.nodeModulesRealpathAliases { + path := realpath + for { + if createdDirectories.Has(path) { + for _, aliasFileName := range aliases.paths { + change.Created.Add(lsconv.FileNameToDocumentURI(aliasFileName)) + } + break + } + parent := path.Parent() + if parent == path { + break + } + path = parent } } - } - for uri := range additionalDeleted.Keys() { - change.Deleted.Add(uri) } return change diff --git a/tsc/internal/project/snapshotfs_test.go b/tsc/internal/project/snapshotfs_test.go index ce087bed9d466..de449f39bb7be 100644 --- a/tsc/internal/project/snapshotfs_test.go +++ b/tsc/internal/project/snapshotfs_test.go @@ -2,6 +2,7 @@ package project import ( "slices" + "strconv" "sync" "testing" @@ -1183,7 +1184,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { change := FileChangeSummary{} change.Changed.Add("file:///packages/mylib/package.json") - expanded := snapshot.expandRealpathAliases(change) + expanded := snapshot.expandRealpathAliases(change, builder.fs) // Should now also contain the symlink path. assert.Assert(t, expanded.Changed.Has("file:///packages/mylib/package.json"), "original event should remain") @@ -1213,7 +1214,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { change := FileChangeSummary{} change.Deleted.Add("file:///packages/mylib/package.json") - expanded := snapshot.expandRealpathAliases(change) + expanded := snapshot.expandRealpathAliases(change, builder.fs) assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules/mylib/package.json"), "symlink deletion should be added") }) @@ -1228,11 +1229,56 @@ func TestRealpathAliasLifecycle(t *testing.T) { change := FileChangeSummary{} change.Changed.Add("file:///some/file.ts") - expanded := snapshot.expandRealpathAliases(change) + expanded := snapshot.expandRealpathAliases(change, snapshot.fs) assert.Equal(t, expanded.Changed.Len(), 1) assert.Assert(t, expanded.Changed.Has("file:///some/file.ts")) }) + t.Run("created path uses current filesystem kind", func(t *testing.T) { + t.Parallel() + files := map[string]any{ + "/project/node_modules/pkg": vfstest.Symlink("/packages/pkg"), + } + for i := range 1001 { + files["/packages/pkg/file"+strconv.Itoa(i)+".ts"] = "export {}" + } + testFS := vfstest.FromMap(files, tspath.CaseSensitive) + builder1 := newSnapshotFSBuilder( + testFS, + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*cachedFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), + nil, + lsproto.PositionEncodingKindUTF16, + ) + for i := range 1001 { + assert.Assert(t, builder1.GetFile(tspath.RootedFilePathFromAbsolute("/project/node_modules/pkg/file"+strconv.Itoa(i)+".ts")) != nil) + } + assert.Assert(t, builder1.fs.DirectoryExists(tspath.RootedDirectoryPath("/packages/pkg"))) + snapshot1, _ := builder1.Finalize() + + assert.NilError(t, testFS.Remove("/packages/pkg")) + assert.NilError(t, testFS.WriteFile("/packages/pkg", "replacement file")) + builder2 := newSnapshotFSBuilder( + testFS, + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + snapshot1.cacheFiles, + snapshot1.cacheDirectories, + snapshot1.nodeModulesRealpathAliases, + lsproto.PositionEncodingKindUTF16, + ) + change := FileChangeSummary{} + change.Created.Add("file:///packages/pkg") + + expanded := snapshot1.expandRealpathAliases(change, builder2.fs) + + assert.Equal(t, expanded.Created.Len(), 1) + assert.Assert(t, expanded.Created.Has("file:///packages/pkg")) + assert.Assert(t, !expanded.HasExcessiveWatchEvents()) + }) + t.Run("markDirtyFiles invalidates symlinked file via realpath event", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ @@ -1274,7 +1320,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { change.Changed.Add("file:///packages/mylib/package.json") // Expand the realpath event to include the symlink path. - change = snapshot1.expandRealpathAliases(change) + change = snapshot1.expandRealpathAliases(change, builder2.fs) // Now mark dirty — should find the file under the symlink key. builder2.markDirtyFiles(change) @@ -1291,6 +1337,132 @@ func TestRealpathAliasLifecycle(t *testing.T) { assert.Equal(t, file.Content(), `{"name": "mylib"}`, "content should be updated") }) + t.Run("created realpath events reload cached symlink alias", func(t *testing.T) { + t.Parallel() + testFS := vfstest.FromMap(map[string]any{ + "/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"), + "/packages/mylib/package.json": `{"name": "mylib", "main": "index.js"}`, + }, tspath.CaseInsensitive) + + builder1 := newSnapshotFSBuilder( + testFS, + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*cachedFile), + make(map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]), + nil, + lsproto.PositionEncodingKindUTF16, + ) + fh := builder1.GetFile("/project/node_modules/mylib/package.json") + assert.Assert(t, fh != nil) + snapshot1, _ := builder1.Finalize() + + err := testFS.WriteFile("/packages/mylib/package.json", `{"name": "mylib"}`) + assert.NilError(t, err) + + builder2 := newSnapshotFSBuilder( + testFS, + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + snapshot1.cacheFiles, + snapshot1.cacheDirectories, + snapshot1.nodeModulesRealpathAliases, + lsproto.PositionEncodingKindUTF16, + ) + change := FileChangeSummary{} + change.Created.Add("file:///packages/mylib") + + change = snapshot1.expandRealpathAliases(change, builder2.fs) + assert.Assert(t, change.Created.Has("file:///project/node_modules/mylib/package.json")) + change = builder2.markDirtyFiles(change) + assert.Assert(t, change.Created.Has("file:///project/node_modules/mylib/package.json")) + + fh = builder2.GetFile("/project/node_modules/mylib/package.json") + assert.Assert(t, fh != nil) + assert.Equal(t, fh.Content(), `{"name": "mylib"}`) + + snapshot2, _ := builder2.Finalize() + err = testFS.WriteFile("/packages/mylib/package.json", `{"name": "updated"}`) + assert.NilError(t, err) + + builder3 := newSnapshotFSBuilder( + testFS, + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + snapshot2.cacheFiles, + snapshot2.cacheDirectories, + snapshot2.nodeModulesRealpathAliases, + lsproto.PositionEncodingKindUTF16, + ) + excessiveChange := FileChangeSummary{IncludesWatchChangeOutsideNodeModules: true} + for i := range 1001 { + excessiveChange.Created.Add(lsproto.DocumentUri("file:///generated/file" + strconv.Itoa(i) + ".ts")) + } + excessiveChange.Created.Add("file:///packages/mylib") + assert.Assert(t, excessiveChange.HasExcessiveWatchEvents()) + excessiveChange = snapshot2.expandRealpathAliases(excessiveChange, builder3.fs) + assert.Assert(t, excessiveChange.Created.Has("file:///project/node_modules/mylib/package.json")) + assert.Assert(t, builder3.watchChangesOverlapCache(excessiveChange, nil, nil)) + + builder3.invalidateCache() + fh = builder3.GetFile("/project/node_modules/mylib/package.json") + assert.Assert(t, fh != nil) + assert.Equal(t, fh.Content(), `{"name": "updated"}`) + }) + + t.Run("markDirtyFiles invalidates existing cached file on created event", func(t *testing.T) { + t.Parallel() + testFS := vfstest.FromMap(map[string]string{ + "/project/src/helper.ts": `export const other = 1;`, + }, tspath.CaseSensitive) + + // Build first snapshot — read the file. + builder1 := newSnapshotFSBuilder( + testFS, + nil, + nil, + nil, + nil, + nil, + lsproto.PositionEncodingKindUTF16, + ) + fh := builder1.GetFile("/project/src/helper.ts") + assert.Assert(t, fh != nil) + assert.Equal(t, fh.Content(), `export const other = 1;`) + snapshot1, _ := builder1.Finalize() + + // Modify the file on disk (e.g. via atomic save rename). + err := testFS.WriteFile("/project/src/helper.ts", `export const thing = 1;`) + assert.NilError(t, err) + + // Build second snapshot — simulate Created event. + builder2 := newSnapshotFSBuilder( + testFS, + nil, + nil, + snapshot1.cacheFiles, + snapshot1.cacheDirectories, + snapshot1.nodeModulesRealpathAliases, + lsproto.PositionEncodingKindUTF16, + ) + + change := FileChangeSummary{} + change.Created.Add("file:///project/src/helper.ts") + + change = builder2.markDirtyFiles(change) + assert.Assert(t, change.Created.Has("file:///project/src/helper.ts")) + + // Reading the file should return the updated content. + fh = builder2.GetFile("/project/src/helper.ts") + assert.Assert(t, fh != nil) + assert.Equal(t, fh.Content(), `export const thing = 1;`) + + snapshot2, _ := builder2.Finalize() + file, ok := snapshot2.cacheFiles[tspath.PathKey("/project/src/helper.ts")] + assert.Assert(t, ok) + assert.Equal(t, file.Content(), `export const thing = 1;`) + }) + t.Run("alias clone isolation between snapshots", func(t *testing.T) { t.Parallel() testFS := vfstest.FromMap(map[string]any{ From 66d345d3ebcf2fe90db553a823ad595c6f539e40 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:10:19 -0500 Subject: [PATCH 2/5] address copilot feedback --- tsc/internal/project/snapshot.go | 1 + tsc/internal/project/snapshotfs.go | 3 ++ tsc/internal/project/snapshotfs_test.go | 49 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index f77681ca69b95..a506f1fac3281 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -180,6 +180,7 @@ func (s *Snapshot) processFileChanges( } _, contentMapperWatchedFiles := s.contentMapperWatchState() fileChanges = fs.expandAndFilterWatchEvents(fileChanges, contentMapperExtensions, contentMapperWatchedFiles, previousOpenFiles, openFiles) + fileChanges = s.fs.expandRealpathAliases(fileChanges, fs.fs) fileChanges = fs.markDirtyFiles(fileChanges) fileChanges = fs.convertOpenAndCloseToChanges(fileChanges, previousOpenFiles, openFiles) } diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index 898afb553e278..1f02321c301da 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -654,6 +654,9 @@ func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary, currentFS L createdDirectories.Add(s.caseSensitivity.PathKey(uri.FileName().AsPath())) } } + if createdDirectories.Len() == 0 { + return change + } for realpath, aliases := range s.nodeModulesRealpathAliases { path := realpath for { diff --git a/tsc/internal/project/snapshotfs_test.go b/tsc/internal/project/snapshotfs_test.go index de449f39bb7be..bfc7c907f19cf 100644 --- a/tsc/internal/project/snapshotfs_test.go +++ b/tsc/internal/project/snapshotfs_test.go @@ -1219,6 +1219,55 @@ func TestRealpathAliasLifecycle(t *testing.T) { assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules/mylib/package.json"), "symlink deletion should be added") }) + t.Run("directory delete invalidates cached realpath and alias", func(t *testing.T) { + t.Parallel() + realpath := tspath.PathKey("/packages/mylib/package.json") + aliasPath := tspath.PathKey("/project/node_modules/mylib/package.json") + aliases := map[tspath.PathKey]*realpathAliasSet{ + realpath: { + paths: aliasPaths{ + aliasPath: tspath.RootedFilePath("/project/node_modules/mylib/package.json"), + }, + }, + } + snapshotFS := &SnapshotFS{ + caseSensitivity: tspath.CaseInsensitive, + cacheFiles: map[tspath.PathKey]*cachedFile{ + realpath: newCachedFile(tspath.RootedFilePath("/packages/mylib/package.json"), `{"name":"mylib"}`), + aliasPath: newCachedFile(tspath.RootedFilePath("/project/node_modules/mylib/package.json"), `{"name":"mylib"}`), + }, + cacheDirectories: map[tspath.PathKey]dirty.CloneableMap[tspath.PathKey, string]{ + tspath.PathKey("/packages/mylib"): {realpath: "package.json"}, + }, + nodeModulesRealpathAliases: aliases, + } + builder := newSnapshotFSBuilder( + vfstest.FromMap(map[string]string{}, tspath.CaseInsensitive), + make(map[tspath.PathKey]*Overlay), + make(map[tspath.PathKey]*Overlay), + snapshotFS.cacheFiles, + snapshotFS.cacheDirectories, + aliases, + lsproto.PositionEncodingKindUTF16, + ) + snapshot := &Snapshot{ + fs: snapshotFS, + ProjectCollection: &ProjectCollection{}, + ConfigFileRegistry: &ConfigFileRegistry{}, + } + change := FileChangeSummary{} + change.Deleted.Add("file:///packages/mylib") + + change = snapshot.processFileChanges(builder, change, nil, nil, nil, nil) + + assert.Assert(t, change.Deleted.Has("file:///packages/mylib/package.json")) + assert.Assert(t, change.Deleted.Has("file:///project/node_modules/mylib/package.json")) + _, realpathCached := builder.cacheFiles.Load(realpath) + _, aliasCached := builder.cacheFiles.Load(aliasPath) + assert.Assert(t, !realpathCached) + assert.Assert(t, !aliasCached) + }) + t.Run("expandRealpathAliases is a no-op with no aliases", func(t *testing.T) { t.Parallel() snapshot := &SnapshotFS{ From 048c83eb91ddd3879d87d4efcd08ed9ea716766a Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:50:06 -0500 Subject: [PATCH 3/5] use collectfilesrecursive --- tsc/internal/project/snapshotfs.go | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index 1f02321c301da..6671b07e41265 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -494,23 +494,9 @@ func (s *snapshotFSBuilder) expandCreatedCacheEntries(change FileChangeSummary) if createdDirectories.Len() == 0 { return change } - s.cacheFiles.Range(func(entry *dirty.SyncMapEntry[tspath.PathKey, *cachedFile]) bool { - path := entry.Key() - for { - if createdDirectories.Has(path) { - if file := entry.Value(); file != nil { - change.Created.Add(lsconv.FileNameToDocumentURI(file.FileName())) - } - break - } - parent := path.Parent() - if parent == path { - break - } - path = parent - } - return true - }) + for path := range createdDirectories.Keys() { + s.collectFilesRecursive(path, &change.Created, nil, nil) + } return change } From 0dbaf982e0937ac0af40334a32002297d1c20a47 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:26:53 -0500 Subject: [PATCH 4/5] addressed feedback --- tsc/internal/project/snapshot.go | 7 +++--- tsc/internal/project/snapshotfs.go | 21 +++++++--------- tsc/internal/project/snapshotfs_test.go | 32 +++++++++++++++---------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index a506f1fac3281..70bcec79efb12 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -144,9 +144,9 @@ func (s *Snapshot) processFileChanges( } previousOpenFiles := overlayFileHandles(previousOverlays) openFiles := overlayFileHandles(overlays) - fileChanges = s.fs.expandRealpathAliases(fileChanges, fs.fs) - fileChanges = fs.expandCreatedCacheEntries(fileChanges) if fileChanges.HasExcessiveWatchEvents() { + fileChanges = fs.expandCreatedCacheEntries(fileChanges) + fileChanges = s.fs.expandRealpathAliases(fileChanges) invalidateStart := time.Now() if fileChanges.InvalidateAll { fs.invalidateCache() @@ -180,7 +180,8 @@ func (s *Snapshot) processFileChanges( } _, contentMapperWatchedFiles := s.contentMapperWatchState() fileChanges = fs.expandAndFilterWatchEvents(fileChanges, contentMapperExtensions, contentMapperWatchedFiles, previousOpenFiles, openFiles) - fileChanges = s.fs.expandRealpathAliases(fileChanges, fs.fs) + fileChanges = fs.expandCreatedCacheEntries(fileChanges) + fileChanges = s.fs.expandRealpathAliases(fileChanges) fileChanges = fs.markDirtyFiles(fileChanges) fileChanges = fs.convertOpenAndCloseToChanges(fileChanges, previousOpenFiles, openFiles) } diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index 6671b07e41265..65f6aa7ce699e 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -487,13 +487,11 @@ func (s *snapshotFSBuilder) expandCreatedCacheEntries(change FileChangeSummary) } createdDirectories := collections.NewSetWithSizeHint[tspath.PathKey](change.Created.Len()) for uri := range change.Created.Keys() { - if s.fs.DirectoryExists(tspath.RootedDirectoryPathFromPath(uri.FileName().AsPath())) { - createdDirectories.Add(s.caseSensitivity.PathKey(uri.FileName().AsPath())) + path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) + if _, ok := s.cacheDirectories.Get(path); ok { + createdDirectories.Add(path) } } - if createdDirectories.Len() == 0 { - return change - } for path := range createdDirectories.Keys() { s.collectFilesRecursive(path, &change.Created, nil, nil) } @@ -530,6 +528,10 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) FileChangeS for uri := range changes.Keys() { path := s.caseSensitivity.PathKey(uri.FileName().AsPath()) if created { + // Cached Created paths represent replacements of files we already knew about. + // If the disk content is unchanged, registries already contain the path and + // there is no filesystem change to propagate. Open overlays remain authoritative, + // so a Created event cannot change their active contents either. if s.fs.Overlays()[path] != nil { continue } @@ -606,7 +608,7 @@ func (s *snapshotFSBuilder) reloadEntryIfContentChanged(entry *dirty.SyncMapEntr // files that were accessed through node_modules symlinks. When a watch event arrives // using a realpath, this expands it to include the symlink-based path so that // downstream consumers (markDirtyFiles, markFilesChanged) can find cached entries. -func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary, currentFS LayeredFileSystem) FileChangeSummary { +func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary) FileChangeSummary { if len(s.nodeModulesRealpathAliases) == 0 { return change } @@ -636,12 +638,7 @@ func (s *SnapshotFS) expandRealpathAliases(change FileChangeSummary, currentFS L if change.Created.Len() > 0 { createdDirectories := collections.NewSetWithSizeHint[tspath.PathKey](change.Created.Len()) for uri := range change.Created.Keys() { - if currentFS.DirectoryExists(tspath.RootedDirectoryPathFromPath(uri.FileName().AsPath())) { - createdDirectories.Add(s.caseSensitivity.PathKey(uri.FileName().AsPath())) - } - } - if createdDirectories.Len() == 0 { - return change + createdDirectories.Add(s.caseSensitivity.PathKey(uri.FileName().AsPath())) } for realpath, aliases := range s.nodeModulesRealpathAliases { path := realpath diff --git a/tsc/internal/project/snapshotfs_test.go b/tsc/internal/project/snapshotfs_test.go index bfc7c907f19cf..15ec85f76851d 100644 --- a/tsc/internal/project/snapshotfs_test.go +++ b/tsc/internal/project/snapshotfs_test.go @@ -1184,7 +1184,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { change := FileChangeSummary{} change.Changed.Add("file:///packages/mylib/package.json") - expanded := snapshot.expandRealpathAliases(change, builder.fs) + expanded := snapshot.expandRealpathAliases(change) // Should now also contain the symlink path. assert.Assert(t, expanded.Changed.Has("file:///packages/mylib/package.json"), "original event should remain") @@ -1214,7 +1214,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { change := FileChangeSummary{} change.Deleted.Add("file:///packages/mylib/package.json") - expanded := snapshot.expandRealpathAliases(change, builder.fs) + expanded := snapshot.expandRealpathAliases(change) assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules/mylib/package.json"), "symlink deletion should be added") }) @@ -1278,15 +1278,16 @@ func TestRealpathAliasLifecycle(t *testing.T) { change := FileChangeSummary{} change.Changed.Add("file:///some/file.ts") - expanded := snapshot.expandRealpathAliases(change, snapshot.fs) + expanded := snapshot.expandRealpathAliases(change) assert.Equal(t, expanded.Changed.Len(), 1) assert.Assert(t, expanded.Changed.Has("file:///some/file.ts")) }) - t.Run("created path uses current filesystem kind", func(t *testing.T) { + t.Run("created cache expansion does not change excessive event classification", func(t *testing.T) { t.Parallel() files := map[string]any{ "/project/node_modules/pkg": vfstest.Symlink("/packages/pkg"), + "/project/src/index.ts": "export {}", } for i := range 1001 { files["/packages/pkg/file"+strconv.Itoa(i)+".ts"] = "export {}" @@ -1304,7 +1305,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { for i := range 1001 { assert.Assert(t, builder1.GetFile(tspath.RootedFilePathFromAbsolute("/project/node_modules/pkg/file"+strconv.Itoa(i)+".ts")) != nil) } - assert.Assert(t, builder1.fs.DirectoryExists(tspath.RootedDirectoryPath("/packages/pkg"))) + assert.Assert(t, builder1.GetFile("/project/src/index.ts") != nil) snapshot1, _ := builder1.Finalize() assert.NilError(t, testFS.Remove("/packages/pkg")) @@ -1318,14 +1319,19 @@ func TestRealpathAliasLifecycle(t *testing.T) { snapshot1.nodeModulesRealpathAliases, lsproto.PositionEncodingKindUTF16, ) - change := FileChangeSummary{} + snapshot := &Snapshot{ + fs: snapshot1, + ProjectCollection: &ProjectCollection{}, + ConfigFileRegistry: &ConfigFileRegistry{}, + } + change := FileChangeSummary{IncludesWatchChangeOutsideNodeModules: true} change.Created.Add("file:///packages/pkg") - expanded := snapshot1.expandRealpathAliases(change, builder2.fs) + change = snapshot.processFileChanges(builder2, change, nil, nil, nil, nil) - assert.Equal(t, expanded.Created.Len(), 1) - assert.Assert(t, expanded.Created.Has("file:///packages/pkg")) - assert.Assert(t, !expanded.HasExcessiveWatchEvents()) + assert.Assert(t, change.HasExcessiveWatchEvents()) + _, unrelatedCached := builder2.cacheFiles.Load(tspath.PathKey("/project/src/index.ts")) + assert.Assert(t, unrelatedCached, "the original event batch should use the normal path") }) t.Run("markDirtyFiles invalidates symlinked file via realpath event", func(t *testing.T) { @@ -1369,7 +1375,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { change.Changed.Add("file:///packages/mylib/package.json") // Expand the realpath event to include the symlink path. - change = snapshot1.expandRealpathAliases(change, builder2.fs) + change = snapshot1.expandRealpathAliases(change) // Now mark dirty — should find the file under the symlink key. builder2.markDirtyFiles(change) @@ -1421,7 +1427,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { change := FileChangeSummary{} change.Created.Add("file:///packages/mylib") - change = snapshot1.expandRealpathAliases(change, builder2.fs) + change = snapshot1.expandRealpathAliases(change) assert.Assert(t, change.Created.Has("file:///project/node_modules/mylib/package.json")) change = builder2.markDirtyFiles(change) assert.Assert(t, change.Created.Has("file:///project/node_modules/mylib/package.json")) @@ -1449,7 +1455,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { } excessiveChange.Created.Add("file:///packages/mylib") assert.Assert(t, excessiveChange.HasExcessiveWatchEvents()) - excessiveChange = snapshot2.expandRealpathAliases(excessiveChange, builder3.fs) + excessiveChange = snapshot2.expandRealpathAliases(excessiveChange) assert.Assert(t, excessiveChange.Created.Has("file:///project/node_modules/mylib/package.json")) assert.Assert(t, builder3.watchChangesOverlapCache(excessiveChange, nil, nil)) From f20f552c032066905a634c45a957c3d681b6a141 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:37:24 -0500 Subject: [PATCH 5/5] fix formatting --- tsc/internal/project/snapshotfs_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsc/internal/project/snapshotfs_test.go b/tsc/internal/project/snapshotfs_test.go index 15ec85f76851d..0910c865f13bd 100644 --- a/tsc/internal/project/snapshotfs_test.go +++ b/tsc/internal/project/snapshotfs_test.go @@ -1287,7 +1287,7 @@ func TestRealpathAliasLifecycle(t *testing.T) { t.Parallel() files := map[string]any{ "/project/node_modules/pkg": vfstest.Symlink("/packages/pkg"), - "/project/src/index.ts": "export {}", + "/project/src/index.ts": "export {}", } for i := range 1001 { files["/packages/pkg/file"+strconv.Itoa(i)+".ts"] = "export {}"