Files
kjol/tools/tsgo/internal/project/snapshotfs_test.go
2026-07-09 16:50:43 -04:00

1529 lines
53 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package project
import (
"slices"
"sync"
"testing"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/project/dirty"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
"gotest.tools/v3/assert"
)
func TestSnapshotFSBuilder(t *testing.T) {
t.Parallel()
toPath := func(fileName string) tspath.Path {
return tspath.Path(fileName)
}
t.Run("builds directory tree on file add", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "const foo = 1;",
}, false /* useCaseSensitiveFileNames */)
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
make(map[tspath.Path]*Overlay), // overlays
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Read the file to add it to the diskFiles
fh := builder.GetFile("/src/foo.ts")
assert.Assert(t, fh != nil, "file should exist")
assert.Equal(t, fh.Content(), "const foo = 1;")
// Finalize and check directories
snapshot, changed := builder.Finalize()
assert.Assert(t, changed, "should have changed")
// Check that directory structure was built
// /src should contain /src/foo.ts
srcDir, ok := snapshot.diskDirectories[tspath.Path("/src")]
assert.Assert(t, ok, "/src directory should exist")
_, hasFoo := srcDir[tspath.Path("/src/foo.ts")]
assert.Assert(t, hasFoo, "/src should contain /src/foo.ts")
// / should contain /src
rootDir, ok := snapshot.diskDirectories[tspath.Path("/")]
assert.Assert(t, ok, "/ directory should exist")
_, hasSrc := rootDir[tspath.Path("/src")]
assert.Assert(t, hasSrc, "/ should contain /src")
})
t.Run("builds nested directory tree", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/nested/deep/file.ts": "export const x = 1;",
}, false /* useCaseSensitiveFileNames */)
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
make(map[tspath.Path]*Overlay), // overlays
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Read the file to add it to the diskFiles
fh := builder.GetFile("/src/nested/deep/file.ts")
assert.Assert(t, fh != nil, "file should exist")
snapshot, changed := builder.Finalize()
assert.Assert(t, changed, "should have changed")
// Check the complete directory tree
_, hasFile := snapshot.diskDirectories[tspath.Path("/src/nested/deep")][tspath.Path("/src/nested/deep/file.ts")]
assert.Assert(t, hasFile)
_, hasDeep := snapshot.diskDirectories[tspath.Path("/src/nested")][tspath.Path("/src/nested/deep")]
assert.Assert(t, hasDeep)
_, hasNested := snapshot.diskDirectories[tspath.Path("/src")][tspath.Path("/src/nested")]
assert.Assert(t, hasNested)
_, hasSrc := snapshot.diskDirectories[tspath.Path("/")][tspath.Path("/src")]
assert.Assert(t, hasSrc)
})
t.Run("removes directory entries on file delete", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "const foo = 1;",
}, false /* useCaseSensitiveFileNames */)
// Start with existing diskFiles and directories
existingDiskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"),
}
existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{
tspath.Path("/"): {
tspath.Path("/src"): "src",
},
tspath.Path("/src"): {
tspath.Path("/src/foo.ts"): "foo.ts",
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
make(map[tspath.Path]*Overlay), // overlays
existingDiskFiles,
existingDirs,
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Mark the file for deletion by loading and deleting
if entry, ok := builder.diskFiles.Load(tspath.Path("/src/foo.ts")); ok {
entry.Delete()
}
snapshot, changed := builder.Finalize()
assert.Assert(t, changed, "should have changed")
// File should be deleted
_, hasFile := snapshot.diskFiles[tspath.Path("/src/foo.ts")]
assert.Assert(t, !hasFile, "file should be deleted")
// Directory tree should be cleaned up
_, hasSrcDir := snapshot.diskDirectories[tspath.Path("/src")]
assert.Assert(t, !hasSrcDir, "/src directory should be removed")
_, hasRootDir := snapshot.diskDirectories[tspath.Path("/")]
assert.Assert(t, !hasRootDir, "root directory should be removed")
})
t.Run("removes only empty directories on file delete", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "const foo = 1;",
"/src/bar.ts": "const bar = 2;",
}, false /* useCaseSensitiveFileNames */)
// Start with existing diskFiles and directories
existingDiskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"),
tspath.Path("/src/bar.ts"): newDiskFile("/src/bar.ts", "const bar = 2;"),
}
existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{
tspath.Path("/"): {
tspath.Path("/src"): "src",
},
tspath.Path("/src"): {
tspath.Path("/src/foo.ts"): "foo.ts",
tspath.Path("/src/bar.ts"): "bar.ts",
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
make(map[tspath.Path]*Overlay), // overlays
existingDiskFiles,
existingDirs,
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Delete only foo.ts
if entry, ok := builder.diskFiles.Load(tspath.Path("/src/foo.ts")); ok {
entry.Delete()
}
snapshot, changed := builder.Finalize()
assert.Assert(t, changed, "should have changed")
// foo.ts should be deleted
_, hasFile := snapshot.diskFiles[tspath.Path("/src/foo.ts")]
assert.Assert(t, !hasFile, "foo.ts should be deleted")
// bar.ts should still exist
_, hasBar := snapshot.diskFiles[tspath.Path("/src/bar.ts")]
assert.Assert(t, hasBar, "bar.ts should still exist")
// /src directory should still exist with bar.ts
srcDir, hasSrcDir := snapshot.diskDirectories[tspath.Path("/src")]
assert.Assert(t, hasSrcDir, "/src directory should still exist")
_, hasFoo := srcDir[tspath.Path("/src/foo.ts")]
assert.Assert(t, !hasFoo, "/src should not contain foo.ts")
_, hasBarInDir := srcDir[tspath.Path("/src/bar.ts")]
assert.Assert(t, hasBarInDir, "/src should contain bar.ts")
// root should still contain /src
rootDir, hasRootDir := snapshot.diskDirectories[tspath.Path("/")]
assert.Assert(t, hasRootDir, "root directory should still exist")
_, hasSrc := rootDir[tspath.Path("/src")]
assert.Assert(t, hasSrc, "root should contain /src")
})
t.Run("adds file to existing directory", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "const foo = 1;",
"/src/bar.ts": "const bar = 2;",
}, false /* useCaseSensitiveFileNames */)
// Start with existing file and directories
existingDiskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"),
}
existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{
tspath.Path("/"): {
tspath.Path("/src"): "src",
},
tspath.Path("/src"): {
tspath.Path("/src/foo.ts"): "foo.ts",
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
make(map[tspath.Path]*Overlay), // overlays
existingDiskFiles,
existingDirs,
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Read bar.ts to add it
fh := builder.GetFile("/src/bar.ts")
assert.Assert(t, fh != nil, "bar.ts should exist")
snapshot, changed := builder.Finalize()
assert.Assert(t, changed, "should have changed")
// /src should contain both files
srcDir := snapshot.diskDirectories[tspath.Path("/src")]
_, hasFoo := srcDir[tspath.Path("/src/foo.ts")]
assert.Assert(t, hasFoo, "/src should contain foo.ts")
_, hasBar := srcDir[tspath.Path("/src/bar.ts")]
assert.Assert(t, hasBar, "/src should contain bar.ts")
})
t.Run("no change when no files added or deleted", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "const foo = 1;",
}, false /* useCaseSensitiveFileNames */)
existingDiskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"),
}
existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{
tspath.Path("/"): {
tspath.Path("/src"): "src",
},
tspath.Path("/src"): {
tspath.Path("/src/foo.ts"): "foo.ts",
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
make(map[tspath.Path]*Overlay), // overlays
existingDiskFiles,
existingDirs,
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Don't add or delete any files
snapshot, changed := builder.Finalize()
assert.Assert(t, !changed, "should not have changed")
// Directories should remain the same
srcDir := snapshot.diskDirectories[tspath.Path("/src")]
_, hasFoo := srcDir[tspath.Path("/src/foo.ts")]
assert.Assert(t, hasFoo)
})
t.Run("overlay files are returned over disk files", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "const foo = 1;",
}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/foo.ts"): {
fileBase: fileBase{fileName: "/src/foo.ts", content: "const foo = 999;"},
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
overlays,
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Should return overlay content
fh := builder.GetFile("/src/foo.ts")
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), "const foo = 999;")
})
t.Run("multiple files added and deleted in single cycle", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/a.ts": "const a = 1;",
"/src/b.ts": "const b = 2;",
"/lib/utils.ts": "export const util = 1;",
"/lib/helpers.ts": "export const helper = 1;",
"/other/single.ts": "const single = 1;",
}, false /* useCaseSensitiveFileNames */)
// Start with some existing files
existingDiskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/a.ts"): newDiskFile("/src/a.ts", "const a = 1;"),
tspath.Path("/other/single.ts"): newDiskFile("/other/single.ts", "const single = 1;"),
}
existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{
tspath.Path("/"): {
tspath.Path("/src"): "src",
tspath.Path("/other"): "other",
},
tspath.Path("/src"): {
tspath.Path("/src/a.ts"): "a.ts",
},
tspath.Path("/other"): {
tspath.Path("/other/single.ts"): "single.ts",
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
make(map[tspath.Path]*Overlay), // overlays
existingDiskFiles,
existingDirs,
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Add new files
fh := builder.GetFile("/src/b.ts")
assert.Assert(t, fh != nil)
fh = builder.GetFile("/lib/utils.ts")
assert.Assert(t, fh != nil)
fh = builder.GetFile("/lib/helpers.ts")
assert.Assert(t, fh != nil)
// Delete existing files
if entry, ok := builder.diskFiles.Load(tspath.Path("/src/a.ts")); ok {
entry.Delete()
}
if entry, ok := builder.diskFiles.Load(tspath.Path("/other/single.ts")); ok {
entry.Delete()
}
snapshot, changed := builder.Finalize()
assert.Assert(t, changed, "should have changed")
// Verify deleted files are gone
_, hasA := snapshot.diskFiles[tspath.Path("/src/a.ts")]
assert.Assert(t, !hasA, "/src/a.ts should be deleted")
_, hasSingle := snapshot.diskFiles[tspath.Path("/other/single.ts")]
assert.Assert(t, !hasSingle, "/other/single.ts should be deleted")
// Verify added files exist
_, hasB := snapshot.diskFiles[tspath.Path("/src/b.ts")]
assert.Assert(t, hasB, "/src/b.ts should exist")
_, hasUtils := snapshot.diskFiles[tspath.Path("/lib/utils.ts")]
assert.Assert(t, hasUtils, "/lib/utils.ts should exist")
_, hasHelpers := snapshot.diskFiles[tspath.Path("/lib/helpers.ts")]
assert.Assert(t, hasHelpers, "/lib/helpers.ts should exist")
// Verify /other directory is cleaned up (was only entry deleted)
_, hasOther := snapshot.diskDirectories[tspath.Path("/other")]
assert.Assert(t, !hasOther, "/other directory should be removed")
// Verify /src still exists with b.ts (a.ts deleted, b.ts added)
srcDir, hasSrc := snapshot.diskDirectories[tspath.Path("/src")]
assert.Assert(t, hasSrc, "/src directory should exist")
_, hasAInDir := srcDir[tspath.Path("/src/a.ts")]
assert.Assert(t, !hasAInDir, "/src should not contain a.ts")
_, hasBInDir := srcDir[tspath.Path("/src/b.ts")]
assert.Assert(t, hasBInDir, "/src should contain b.ts")
// Verify /lib was created with both files
libDir, hasLib := snapshot.diskDirectories[tspath.Path("/lib")]
assert.Assert(t, hasLib, "/lib directory should exist")
_, hasUtilsInDir := libDir[tspath.Path("/lib/utils.ts")]
assert.Assert(t, hasUtilsInDir, "/lib should contain utils.ts")
_, hasHelpersInDir := libDir[tspath.Path("/lib/helpers.ts")]
assert.Assert(t, hasHelpersInDir, "/lib should contain helpers.ts")
// Verify root contains /src and /lib but not /other
rootDir := snapshot.diskDirectories[tspath.Path("/")]
_, hasSrcInRoot := rootDir[tspath.Path("/src")]
assert.Assert(t, hasSrcInRoot, "root should contain /src")
_, hasLibInRoot := rootDir[tspath.Path("/lib")]
assert.Assert(t, hasLibInRoot, "root should contain /lib")
_, hasOtherInRoot := rootDir[tspath.Path("/other")]
assert.Assert(t, !hasOtherInRoot, "root should not contain /other")
})
t.Run("overlay directories are computed from overlays", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/overlay.ts"): {
fileBase: fileBase{fileName: "/src/overlay.ts", content: "const x = 1;"},
},
tspath.Path("/src/nested/deep.ts"): {
fileBase: fileBase{fileName: "/src/nested/deep.ts", content: "const y = 2;"},
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
overlays,
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Check overlayDirectories was built correctly
srcDir, ok := builder.overlayDirectories[tspath.Path("/src")]
assert.Assert(t, ok, "/src overlay directory should exist")
_, hasOverlay := srcDir[tspath.Path("/src/overlay.ts")]
assert.Assert(t, hasOverlay, "/src should contain overlay.ts")
_, hasNested := srcDir[tspath.Path("/src/nested")]
assert.Assert(t, hasNested, "/src should contain nested/")
nestedDir, ok := builder.overlayDirectories[tspath.Path("/src/nested")]
assert.Assert(t, ok, "/src/nested overlay directory should exist")
_, hasDeep := nestedDir[tspath.Path("/src/nested/deep.ts")]
assert.Assert(t, hasDeep, "/src/nested should contain deep.ts")
rootDir, ok := builder.overlayDirectories[tspath.Path("/")]
assert.Assert(t, ok, "/ overlay directory should exist")
_, hasSrc := rootDir[tspath.Path("/src")]
assert.Assert(t, hasSrc, "/ should contain /src")
})
t.Run("GetAccessibleEntries combines disk and overlay", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/disk.ts": "const disk = 1;",
}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/overlay.ts"): {
fileBase: fileBase{fileName: "/src/overlay.ts", content: "const overlay = 1;"},
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
overlays,
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
entries := builder.GetAccessibleEntries("/src")
// Should contain both disk file and overlay file (both as basenames)
assert.Assert(t, slices.Contains(entries.Files, "disk.ts"), "should contain disk.ts")
assert.Assert(t, slices.Contains(entries.Files, "overlay.ts"), "should contain overlay.ts")
})
t.Run("GetAccessibleEntries is safe under concurrent calls", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/a.ts": "",
"/src/b.ts": "",
"/src/c.ts": "",
"/src/d.ts": "",
"/src/e.ts": "",
}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/overlay.ts"): {
fileBase: fileBase{fileName: "/src/overlay.ts", content: ""},
},
}
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay), // prevOverlays
overlays,
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
_ = builder.fs.GetAccessibleEntries("/src")
start := make(chan struct{})
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
<-start
_ = builder.GetAccessibleEntries("/src")
})
}
close(start)
wg.Wait()
// Sanity: the overlay is still reported after all the concurrent calls.
entries := builder.GetAccessibleEntries("/src")
assert.Assert(t, slices.Contains(entries.Files, "overlay.ts"), "should contain overlay.ts")
})
}
func TestSnapshotFS(t *testing.T) {
t.Parallel()
toPath := func(fileName string) tspath.Path {
return tspath.Path(fileName)
}
t.Run("GetFile returns overlay file", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "disk content",
}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/foo.ts"): {
fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"},
},
}
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: overlays,
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
fh := snapshot.GetFile("/src/foo.ts")
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), "overlay content")
})
t.Run("GetFile returns disk file when not in overlay", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "disk content",
}, false /* useCaseSensitiveFileNames */)
diskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "disk content"),
}
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: diskFiles,
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
fh := snapshot.GetFile("/src/foo.ts")
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), "disk content")
})
t.Run("GetFile reads from fs when not cached", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "fs content",
}, false /* useCaseSensitiveFileNames */)
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
fh := snapshot.GetFile("/src/foo.ts")
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), "fs content")
})
t.Run("GetFile returns nil for non-existent file", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */)
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
fh := snapshot.GetFile("/src/nonexistent.ts")
assert.Assert(t, fh == nil, "should return nil for non-existent file")
})
t.Run("isOpenFile returns true for overlays", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/foo.ts"): {
fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"},
},
}
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: overlays,
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
assert.Assert(t, snapshot.isOpenFile("/src/foo.ts"), "overlay file should be open")
assert.Assert(t, !snapshot.isOpenFile("/src/bar.ts"), "non-overlay file should not be open")
})
t.Run("GetFileByPath uses provided path", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "disk content",
}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/foo.ts"): {
fileBase: fileBase{fileName: "/src/foo.ts", content: "overlay content"},
},
}
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: overlays,
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
// GetFileByPath should use the provided path directly
fh := snapshot.GetFileByPath("/src/foo.ts", tspath.Path("/src/foo.ts"))
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), "overlay content")
})
t.Run("GetAccessibleEntries combines disk and overlay directories", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{}, false /* useCaseSensitiveFileNames */)
overlays := map[tspath.Path]*Overlay{
tspath.Path("/src/overlay.ts"): {
fileBase: fileBase{fileName: "/src/overlay.ts", content: "overlay content"},
},
}
overlayDirectories := map[tspath.Path]map[tspath.Path]string{
tspath.Path("/"): {
tspath.Path("/src"): "src",
},
tspath.Path("/src"): {
tspath.Path("/src/overlay.ts"): "overlay.ts",
},
}
diskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/disk.ts"): newDiskFile("/src/disk.ts", "disk content"),
}
diskDirectories := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{
tspath.Path("/"): {
tspath.Path("/src"): "src",
},
tspath.Path("/src"): {
tspath.Path("/src/disk.ts"): "disk.ts",
},
}
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: overlays,
overlayDirectories: overlayDirectories,
diskFiles: diskFiles,
diskDirectories: diskDirectories,
}
entries := snapshot.GetAccessibleEntries("/src")
// Should contain both disk file and overlay file (both as basenames)
assert.Assert(t, slices.Contains(entries.Files, "disk.ts"), "should contain disk.ts")
assert.Assert(t, slices.Contains(entries.Files, "overlay.ts"), "should contain overlay.ts")
})
}
func TestSourceFS(t *testing.T) {
t.Parallel()
toPath := func(fileName string) tspath.Path {
return tspath.Path(fileName)
}
t.Run("tracks files when tracking enabled", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "content",
}, false /* useCaseSensitiveFileNames */)
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
sourceFS := newSourceFS(true /* tracking */, snapshot, toPath)
// File should not be seen yet
assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/foo.ts")))
// Read the file
fh := sourceFS.GetFile("/src/foo.ts")
assert.Assert(t, fh != nil)
// Now it should be seen
assert.Assert(t, sourceFS.SeenFile(tspath.Path("/src/foo.ts")))
})
t.Run("does not track files when tracking disabled", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "content",
}, false /* useCaseSensitiveFileNames */)
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
sourceFS := newSourceFS(false /* tracking */, snapshot, toPath)
// Read the file
fh := sourceFS.GetFile("/src/foo.ts")
assert.Assert(t, fh != nil)
// Should not be seen since tracking is disabled
assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/foo.ts")))
})
t.Run("DisableTracking stops tracking", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "content",
"/src/bar.ts": "content",
}, false /* useCaseSensitiveFileNames */)
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
sourceFS := newSourceFS(true /* tracking */, snapshot, toPath)
// Read foo while tracking
sourceFS.GetFile("/src/foo.ts")
assert.Assert(t, sourceFS.SeenFile(tspath.Path("/src/foo.ts")))
// Disable tracking
sourceFS.DisableTracking()
// Read bar after tracking disabled
sourceFS.GetFile("/src/bar.ts")
assert.Assert(t, !sourceFS.SeenFile(tspath.Path("/src/bar.ts")))
})
t.Run("FileExists returns true for files in source", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "content",
}, false /* useCaseSensitiveFileNames */)
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
sourceFS := newSourceFS(false /* tracking */, snapshot, toPath)
assert.Assert(t, sourceFS.FileExists("/src/foo.ts"))
assert.Assert(t, !sourceFS.FileExists("/src/nonexistent.ts"))
})
t.Run("ReadFile returns content for files in source", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]string{
"/src/foo.ts": "file content",
}, false /* useCaseSensitiveFileNames */)
snapshot := &SnapshotFS{
toPath: toPath,
fs: testFS,
overlays: make(map[tspath.Path]*Overlay),
overlayDirectories: make(map[tspath.Path]map[tspath.Path]string),
diskFiles: make(map[tspath.Path]*diskFile),
diskDirectories: make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
}
sourceFS := newSourceFS(false /* tracking */, snapshot, toPath)
content, ok := sourceFS.ReadFile("/src/foo.ts")
assert.Assert(t, ok)
assert.Equal(t, content, "file content")
_, ok = sourceFS.ReadFile("/src/nonexistent.ts")
assert.Assert(t, !ok)
})
}
func TestAutoImportBuilderFS(t *testing.T) {
t.Parallel()
toPath := func(fileName string) tspath.Path {
return tspath.Path(fileName)
}
// This test demonstrates that autoImportBuilderFS stores files in untrackedFiles keyed
// by the path derived from the filename. When module resolution reads a file via its
// symlink path, the file is cached at the symlink path key. If the file is subsequently
// requested by its realpath (which is the typical case after module resolution resolves
// the symlink), the cache is bypassed. If the file was deleted from disk between those
// two operations, the realpath read fails and returns nil. This is the mechanism behind
// the nil source file crash in aliasResolver.GetSourceFile.
t.Run("symlink cache mismatch: file cached at symlink path, missed at realpath after deletion", func(t *testing.T) {
t.Parallel()
// Create a VFS with a real file and a symlinked directory pointing to it.
// /real/pkg/index.d.ts is the real file.
// /project/node_modules/pkg is a symlink to /real/pkg.
testFS := vfstest.FromMap(map[string]any{
"/real/pkg/index.d.ts": "export declare const x: number;",
"/project/node_modules/pkg": vfstest.Symlink("/real/pkg"),
}, true /* useCaseSensitiveFileNames */)
// Verify symlink works as expected
symlinkPath := "/project/node_modules/pkg/index.d.ts"
realpathPath := testFS.Realpath(symlinkPath)
assert.Equal(t, realpathPath, "/real/pkg/index.d.ts", "Realpath should resolve the symlink to the real path")
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil, // nodeModulesRealpathAliases
lsproto.PositionEncodingKindUTF16,
toPath,
)
autoImportFS := &autoImportBuilderFS{
snapshotFSBuilder: builder,
}
// Step 1: Read the file via its symlink path (simulating what module resolution does
// during FileExists). This caches the file in untrackedFiles at the symlink path key.
fh := autoImportFS.GetFile(symlinkPath)
assert.Assert(t, fh != nil, "File should be readable via symlink path")
assert.Equal(t, fh.Content(), "export declare const x: number;")
// Step 2: Simulate a file deletion from disk (e.g., npm install running concurrently).
// This deletes both the real file and effectively breaks the symlink.
err := testFS.Remove("/real/pkg/index.d.ts")
assert.NilError(t, err)
// Step 3: Request the file by its realpath (simulating what GetSourceFile does after
// the checker resolves the module). This bypasses the symlink-path cache entry in
// untrackedFiles because the realpath has a different key.
fh2 := autoImportFS.GetFile(realpathPath)
// The file was cached at the symlink path, but the realpath lookup misses the cache
// and goes to disk where the file is now deleted. This returns nil.
assert.Assert(t, fh2 == nil, "File should be nil when accessed by realpath after deletion from disk")
})
}
func TestRealpathAliasLifecycle(t *testing.T) {
t.Parallel()
toPath := func(fileName string) tspath.Path {
return tspath.Path(fileName)
}
t.Run("alias recorded when reading symlinked node_modules file", 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"}`,
"/packages/mylib/index.d.ts": `export declare const x: number;`,
"/project/node_modules/nolink/package.json": `{"name": "nolink"}`,
}, false)
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Read a file through the symlink — should record an alias.
fh := builder.GetFile("/project/node_modules/mylib/package.json")
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), `{"name": "mylib", "main": "index.js"}`)
// Read a non-symlinked node_modules file — should NOT record an alias.
fh2 := builder.GetFile("/project/node_modules/nolink/package.json")
assert.Assert(t, fh2 != nil)
snapshot, _ := builder.Finalize()
// Alias exists for the symlinked file.
aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok, "alias should exist for realpath of symlinked file")
assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")))
// No alias for the non-symlinked file.
_, ok = snapshot.nodeModulesRealpathAliases[tspath.Path("/project/node_modules/nolink/package.json")]
assert.Assert(t, !ok, "no alias should exist for non-symlinked file")
})
t.Run("no alias recorded for files outside node_modules", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]any{
"/project/link": vfstest.Symlink("/elsewhere"),
"/elsewhere/index.ts": `export const x = 1;`,
}, false)
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
fh := builder.GetFile("/project/link/index.ts")
assert.Assert(t, fh != nil)
snapshot, _ := builder.Finalize()
assert.Equal(t, len(snapshot.nodeModulesRealpathAliases), 0, "no aliases for non-node_modules symlinks")
})
t.Run("aliases carried over across snapshots", 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"}`,
}, false)
// Build first snapshot.
builder1 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder1.GetFile("/project/node_modules/mylib/package.json")
snapshot1, _ := builder1.Finalize()
// Build second snapshot from the first, without reading the file again.
builder2 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
snapshot1.diskFiles,
snapshot1.diskDirectories,
snapshot1.nodeModulesRealpathAliases,
lsproto.PositionEncodingKindUTF16,
toPath,
)
snapshot2, _ := builder2.Finalize()
// Alias should still be present.
aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok, "alias should survive across snapshots")
assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")))
})
t.Run("alias pruned when symlinked file is deleted", 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"}`,
"/packages/mylib/index.d.ts": `export declare const x: number;`,
}, false)
// Build first snapshot — read both files.
builder1 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder1.GetFile("/project/node_modules/mylib/package.json")
builder1.GetFile("/project/node_modules/mylib/index.d.ts")
snapshot1, _ := builder1.Finalize()
// Both should be aliased under the same realpath directory but separate files.
_, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok)
_, ok = snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/index.d.ts")]
assert.Assert(t, ok)
// Build second snapshot — delete one file via markDirtyFiles.
builder2 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
snapshot1.diskFiles,
snapshot1.diskDirectories,
snapshot1.nodeModulesRealpathAliases,
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Simulate deletion of index.d.ts from the disk file cache.
var entry *dirty.SyncMapEntry[tspath.Path, *diskFile]
if entry, ok = builder2.diskFiles.Load(tspath.Path("/project/node_modules/mylib/index.d.ts")); ok {
entry.Delete()
}
snapshot2, _ := builder2.Finalize()
// package.json alias should remain.
aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok, "package.json alias should survive")
assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")))
// index.d.ts alias should be fully pruned (empty set → removed from map).
_, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/index.d.ts")]
assert.Assert(t, !ok, "index.d.ts alias should be pruned after deletion")
})
t.Run("multiple symlinks to same realpath", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]any{
"/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"),
"/project/node_modules/alias": vfstest.Symlink("/packages/mylib"),
"/packages/mylib/package.json": `{"name": "mylib"}`,
}, false)
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
// Read via both symlinks.
fh1 := builder.GetFile("/project/node_modules/mylib/package.json")
assert.Assert(t, fh1 != nil)
fh2 := builder.GetFile("/project/node_modules/alias/package.json")
assert.Assert(t, fh2 != nil)
snapshot, _ := builder.Finalize()
aliases, ok := snapshot.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok, "alias should exist")
assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")))
assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/alias/package.json")))
})
t.Run("multiple symlinks pruned individually", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]any{
"/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"),
"/project/node_modules/alias": vfstest.Symlink("/packages/mylib"),
"/packages/mylib/package.json": `{"name": "mylib"}`,
}, false)
// Build first snapshot read via both symlinks.
builder1 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder1.GetFile("/project/node_modules/mylib/package.json")
builder1.GetFile("/project/node_modules/alias/package.json")
snapshot1, _ := builder1.Finalize()
// Build second snapshot delete ONE of the symlink disk entries.
builder2 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
snapshot1.diskFiles,
snapshot1.diskDirectories,
snapshot1.nodeModulesRealpathAliases,
lsproto.PositionEncodingKindUTF16,
toPath,
)
if entry, ok := builder2.diskFiles.Load(tspath.Path("/project/node_modules/alias/package.json")); ok {
entry.Delete()
}
snapshot2, _ := builder2.Finalize()
// The realpath alias set should still exist, but only contain the surviving symlink.
aliases, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok, "alias set should still exist")
assert.Assert(t, aliases.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")), "surviving symlink should remain")
assert.Assert(t, !aliases.paths.Has(tspath.Path("/project/node_modules/alias/package.json")), "deleted symlink should be pruned")
})
t.Run("expandRealpathAliases expands change events", 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"}`,
}, false)
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder.GetFile("/project/node_modules/mylib/package.json")
snapshot, _ := builder.Finalize()
// Simulate a watch event on the REALPATH.
change := FileChangeSummary{}
change.Changed.Add("file:///packages/mylib/package.json")
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")
assert.Assert(t, expanded.Changed.Has("file:///project/node_modules/mylib/package.json"), "symlink event should be added")
})
t.Run("expandRealpathAliases expands delete events", 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"}`,
}, false)
builder := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder.GetFile("/project/node_modules/mylib/package.json")
snapshot, _ := builder.Finalize()
// Simulate a delete watch event on the REALPATH.
change := FileChangeSummary{}
change.Deleted.Add("file:///packages/mylib/package.json")
expanded := snapshot.expandRealpathAliases(change)
assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules/mylib/package.json"), "symlink deletion should be added")
})
t.Run("expandRealpathAliases is a no-op with no aliases", func(t *testing.T) {
t.Parallel()
snapshot := &SnapshotFS{
toPath: toPath,
nodeModulesRealpathAliases: nil,
}
change := FileChangeSummary{}
change.Changed.Add("file:///some/file.ts")
expanded := snapshot.expandRealpathAliases(change)
assert.Equal(t, expanded.Changed.Len(), 1)
assert.Assert(t, expanded.Changed.Has("file:///some/file.ts"))
})
t.Run("markDirtyFiles invalidates symlinked file via realpath event", 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"}`,
}, false)
// Build first snapshot — read the symlinked file.
builder1 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
fh := builder1.GetFile("/project/node_modules/mylib/package.json")
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), `{"name": "mylib", "main": "index.js"}`)
snapshot1, _ := builder1.Finalize()
// Modify the real file on disk.
err := testFS.WriteFile("/packages/mylib/package.json", `{"name": "mylib"}`)
assert.NilError(t, err)
// Build second snapshot — simulate realpath change event, expanded via aliases.
builder2 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
snapshot1.diskFiles,
snapshot1.diskDirectories,
snapshot1.nodeModulesRealpathAliases,
lsproto.PositionEncodingKindUTF16,
toPath,
)
change := FileChangeSummary{}
change.Changed.Add("file:///packages/mylib/package.json")
// Expand the realpath event to include the symlink path.
change = snapshot1.expandRealpathAliases(change)
// Now mark dirty — should find the file under the symlink key.
builder2.markDirtyFiles(change)
// Trigger reload by reading the file (simulates program construction).
fh = builder2.GetFile("/project/node_modules/mylib/package.json")
assert.Assert(t, fh != nil)
assert.Equal(t, fh.Content(), `{"name": "mylib"}`, "builder should serve updated content after dirty marking")
snapshot2, _ := builder2.Finalize()
// The file should have been reloaded with new content.
file, ok := snapshot2.diskFiles[tspath.Path("/project/node_modules/mylib/package.json")]
assert.Assert(t, ok, "file should still be in diskFiles")
assert.Equal(t, file.Content(), `{"name": "mylib"}`, "content should be updated")
})
t.Run("alias clone isolation between snapshots", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]any{
"/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"),
"/project/node_modules/other": vfstest.Symlink("/packages/other"),
"/packages/mylib/package.json": `{"name": "mylib"}`,
"/packages/other/package.json": `{"name": "other"}`,
}, false)
// Build first snapshot — read only mylib.
builder1 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder1.GetFile("/project/node_modules/mylib/package.json")
snapshot1, _ := builder1.Finalize()
// Build second snapshot — also read other.
builder2 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
snapshot1.diskFiles,
snapshot1.diskDirectories,
snapshot1.nodeModulesRealpathAliases,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder2.GetFile("/project/node_modules/other/package.json")
snapshot2, _ := builder2.Finalize()
// snapshot1 should only have mylib alias.
_, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok, "snapshot1 should have mylib alias")
_, ok = snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/other/package.json")]
assert.Assert(t, !ok, "snapshot1 should NOT have other alias — it was added in a later snapshot")
// snapshot2 should have both.
_, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok, "snapshot2 should have mylib alias")
_, ok = snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/other/package.json")]
assert.Assert(t, ok, "snapshot2 should have other alias")
})
t.Run("adding symlink to inherited realpath key does not mutate previous snapshot", func(t *testing.T) {
t.Parallel()
testFS := vfstest.FromMap(map[string]any{
"/project/node_modules/mylib": vfstest.Symlink("/packages/mylib"),
"/project/node_modules/alias": vfstest.Symlink("/packages/mylib"),
"/packages/mylib/package.json": `{"name": "mylib"}`,
}, false)
// Snapshot 1: read via one symlink only.
builder1 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder1.GetFile("/project/node_modules/mylib/package.json")
snapshot1, _ := builder1.Finalize()
// Verify snapshot1 has exactly one alias for the realpath.
aliases1, ok := snapshot1.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok)
assert.Equal(t, aliases1.paths.Len(), 1)
assert.Assert(t, aliases1.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")))
// Snapshot 2: read via the SECOND symlink, which maps to the same realpath.
// This exercises the case where LoadOrStore finds the key in the base map
// and must clone-on-write rather than mutating the shared set.
builder2 := newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
snapshot1.diskFiles,
snapshot1.diskDirectories,
snapshot1.nodeModulesRealpathAliases,
lsproto.PositionEncodingKindUTF16,
toPath,
)
builder2.GetFile("/project/node_modules/alias/package.json")
snapshot2, _ := builder2.Finalize()
// Snapshot 2 should have both symlinks.
aliases2, ok := snapshot2.nodeModulesRealpathAliases[tspath.Path("/packages/mylib/package.json")]
assert.Assert(t, ok)
assert.Equal(t, aliases2.paths.Len(), 2)
assert.Assert(t, aliases2.paths.Has(tspath.Path("/project/node_modules/mylib/package.json")))
assert.Assert(t, aliases2.paths.Has(tspath.Path("/project/node_modules/alias/package.json")))
// Snapshot 1 must NOT have been mutated — it should still have only one alias.
assert.Equal(t, aliases1.paths.Len(), 1, "snapshot1 alias set must not be mutated by snapshot2")
assert.Assert(t, !aliases1.paths.Has(tspath.Path("/project/node_modules/alias/package.json")),
"snapshot1 must not contain alias added in snapshot2")
})
}
func TestExpandAndFilterWatchEvents(t *testing.T) {
t.Parallel()
toPath := func(fileName string) tspath.Path {
return tspath.Path(fileName)
}
newBuilder := func(testFS vfs.FS) *snapshotFSBuilder {
return newSnapshotFSBuilder(
testFS,
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*diskFile),
make(map[tspath.Path]dirty.CloneableMap[tspath.Path, string]),
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
}
t.Run("preserves node_modules directory deletion even when untracked", func(t *testing.T) {
t.Parallel()
// node_modules package files are read transiently and never tracked in
// diskDirectories, so a node_modules directory deletion can neither be
// expanded nor matched by extension. It must still be preserved.
builder := newBuilder(vfstest.FromMap(map[string]string{
"/project/index.ts": "export const x = 1;",
}, false))
change := FileChangeSummary{}
change.Deleted.Add("file:///project/node_modules")
expanded := builder.expandAndFilterWatchEvents(change)
assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules"),
"bare node_modules directory deletion should be preserved")
})
t.Run("preserves deletion of a package directory inside node_modules", func(t *testing.T) {
t.Parallel()
builder := newBuilder(vfstest.FromMap(map[string]string{
"/project/index.ts": "export const x = 1;",
}, false))
change := FileChangeSummary{}
change.Deleted.Add("file:///project/node_modules/@scope/pkg")
expanded := builder.expandAndFilterWatchEvents(change)
assert.Assert(t, expanded.Deleted.Has("file:///project/node_modules/@scope/pkg"),
"package directory deletion inside node_modules should be preserved")
})
t.Run("drops irrelevant untracked deletion outside node_modules", func(t *testing.T) {
t.Parallel()
builder := newBuilder(vfstest.FromMap(map[string]string{
"/project/index.ts": "export const x = 1;",
}, false))
change := FileChangeSummary{}
change.Deleted.Add("file:///project/build")
expanded := builder.expandAndFilterWatchEvents(change)
assert.Equal(t, expanded.Deleted.Len(), 0,
"untracked non-node_modules directory deletion should be dropped")
})
t.Run("expands tracked directory deletion into file deletions", func(t *testing.T) {
t.Parallel()
existingDiskFiles := map[tspath.Path]*diskFile{
tspath.Path("/src/foo.ts"): newDiskFile("/src/foo.ts", "const foo = 1;"),
}
existingDirs := map[tspath.Path]dirty.CloneableMap[tspath.Path, string]{
tspath.Path("/"): {tspath.Path("/src"): "src"},
tspath.Path("/src"): {tspath.Path("/src/foo.ts"): "foo.ts"},
}
builder := newSnapshotFSBuilder(
vfstest.FromMap(map[string]string{"/src/foo.ts": "const foo = 1;"}, false),
make(map[tspath.Path]*Overlay),
make(map[tspath.Path]*Overlay),
existingDiskFiles,
existingDirs,
nil,
lsproto.PositionEncodingKindUTF16,
toPath,
)
change := FileChangeSummary{}
change.Deleted.Add("file:///src")
expanded := builder.expandAndFilterWatchEvents(change)
assert.Assert(t, expanded.Deleted.Has("file:///src/foo.ts"),
"tracked directory deletion should expand to contained file deletions")
assert.Assert(t, !expanded.Deleted.Has("file:///src"),
"the directory URI itself should be replaced by its files")
})
}