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

803 lines
24 KiB
Go

package project
import (
"slices"
"strings"
"sync"
"time"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/ls/lsconv"
"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/cachedvfs"
"github.com/zeebo/xxh3"
)
type FileSource interface {
FS() vfs.FS
GetFile(fileName string) FileHandle
GetFileByPath(fileName string, path tspath.Path) FileHandle
FileExists(fileName string, path tspath.Path) bool
GetAccessibleEntries(path string) vfs.Entries
}
var (
_ FileSource = (*snapshotFSBuilder)(nil)
_ FileSource = (*SnapshotFS)(nil)
)
// realpathAliasSet is a thread-safe set of symlink paths that alias a single realpath.
// It implements dirty.Cloneable so it can be used as a value in dirty.SyncMap.
type realpathAliasSet struct {
mu sync.Mutex
paths collections.Set[tspath.Path]
}
func (s *realpathAliasSet) Add(path tspath.Path) {
s.mu.Lock()
defer s.mu.Unlock()
s.paths.Add(path)
}
func (s *realpathAliasSet) Clone() *realpathAliasSet {
s.mu.Lock()
defer s.mu.Unlock()
clone := &realpathAliasSet{}
if s.paths.Len() > 0 {
clone.paths = *s.paths.Clone()
}
return clone
}
type SnapshotFS struct {
toPath func(fileName string) tspath.Path
fs vfs.FS
overlays map[tspath.Path]*Overlay
overlayDirectories map[tspath.Path]map[tspath.Path]string
diskFiles map[tspath.Path]*diskFile
diskDirectories map[tspath.Path]dirty.CloneableMap[tspath.Path, string]
readFiles collections.SyncMap[tspath.Path, memoizedDiskFile]
// nodeModulesRealpathAliases maps realpath-based keys to sets of symlink-based keys,
// for files inside node_modules that are accessed through directory symlinks.
// This allows watch events (which use realpaths) to invalidate files cached under symlink paths.
nodeModulesRealpathAliases map[tspath.Path]*realpathAliasSet
}
type memoizedDiskFile func() FileHandle
func (s *SnapshotFS) FS() vfs.FS {
return s.fs
}
func (s *SnapshotFS) GetFile(fileName string) FileHandle {
return s.GetFileByPath(fileName, s.toPath(fileName))
}
func (s *SnapshotFS) FileExists(fileName string, path tspath.Path) bool {
if _, ok := s.overlays[path]; ok {
return true
}
if _, ok := s.diskFiles[path]; ok {
return true
}
return s.fs.FileExists(fileName)
}
func (s *SnapshotFS) GetFileByPath(fileName string, path tspath.Path) FileHandle {
if file, ok := s.overlays[path]; ok {
return file
}
if file, ok := s.diskFiles[path]; ok {
return file
}
newEntry := memoizedDiskFile(sync.OnceValue(func() FileHandle {
if contents, ok := s.fs.ReadFile(fileName); ok {
return newDiskFile(fileName, contents)
}
return nil
}))
entry, _ := s.readFiles.LoadOrStore(path, newEntry)
return entry()
}
func (s *SnapshotFS) GetAccessibleEntries(directoryName string) vfs.Entries {
var entries vfs.Entries
path := s.toPath(directoryName)
if diskDirectories, ok := s.diskDirectories[path]; ok {
readDirectoryIntoEntries(diskDirectories, s.isFile, &entries)
}
if overlayDirectories, ok := s.overlayDirectories[path]; ok {
readDirectoryIntoEntries(overlayDirectories, s.isFile, &entries)
}
return entries
}
func (s *SnapshotFS) isOpenFile(fileName string) bool {
path := s.toPath(fileName)
_, ok := s.overlays[path]
return ok
}
func (s *SnapshotFS) isFile(path tspath.Path) bool {
if _, ok := s.diskFiles[path]; ok {
return true
}
if _, ok := s.overlays[path]; ok {
return true
}
return false
}
type snapshotFSBuilder struct {
fs vfs.FS
prevOverlays map[tspath.Path]*Overlay
overlays map[tspath.Path]*Overlay
overlayDirectories map[tspath.Path]map[tspath.Path]string
diskFiles *dirty.SyncMap[tspath.Path, *diskFile]
diskDirectories *dirty.Map[tspath.Path, dirty.CloneableMap[tspath.Path, string]]
nodeModulesRealpathAliases *dirty.SyncMap[tspath.Path, *realpathAliasSet]
toPath func(string) tspath.Path
accessibleEntries collections.SyncMap[tspath.Path, *vfs.Entries]
}
func newSnapshotFSBuilder(
fs vfs.FS,
prevOverlays map[tspath.Path]*Overlay,
overlays map[tspath.Path]*Overlay,
diskFiles map[tspath.Path]*diskFile,
diskDirectories map[tspath.Path]dirty.CloneableMap[tspath.Path, string],
nodeModulesRealpathAliases map[tspath.Path]*realpathAliasSet,
positionEncoding lsproto.PositionEncodingKind,
toPath func(fileName string) tspath.Path,
) *snapshotFSBuilder {
cachedFS := cachedvfs.From(fs)
cachedFS.Enable()
overlayDirectories := make(map[tspath.Path]map[tspath.Path]string)
for path := range overlays {
childPath := path
child := overlays[path].FileName()
for {
parentPath := childPath.GetDirectoryPath()
parent := tspath.GetDirectoryPath(child)
if childPath == parentPath {
break // reached root
}
baseName := tspath.GetBaseFileName(child)
if dir, ok := overlayDirectories[parentPath]; ok {
dir[childPath] = baseName
} else {
dir := make(map[tspath.Path]string)
overlayDirectories[parentPath] = dir
dir[childPath] = baseName
}
childPath = parentPath
child = parent
}
}
return &snapshotFSBuilder{
fs: cachedFS,
prevOverlays: prevOverlays,
overlays: overlays,
overlayDirectories: overlayDirectories,
diskFiles: dirty.NewSyncMap(diskFiles),
diskDirectories: dirty.NewMap(diskDirectories),
nodeModulesRealpathAliases: dirty.NewSyncMap(nodeModulesRealpathAliases),
toPath: toPath,
}
}
func (s *snapshotFSBuilder) FS() vfs.FS {
return s.fs
}
func (s *snapshotFSBuilder) Finalize() (*SnapshotFS, bool) {
// Synchronize directory structure based on added and deleted files (including overlays)
var onDeletedFileOrDirectory func(path tspath.Path)
var deleted map[tspath.Path]*diskFile
onAddedFile := func(path tspath.Path, fileName string) {
childPath := path
child := fileName
for {
parentPath := childPath.GetDirectoryPath()
parent := tspath.GetDirectoryPath(child)
if childPath == parentPath {
break // reached root
}
baseName := tspath.GetBaseFileName(child)
if dirEntry, ok := s.diskDirectories.Get(parentPath); ok {
dirEntry.Change(func(dir dirty.CloneableMap[tspath.Path, string]) {
dir[childPath] = baseName
})
break
} else {
dir := make(dirty.CloneableMap[tspath.Path, string])
dir[childPath] = baseName
s.diskDirectories.Add(parentPath, dir)
}
childPath = parentPath
child = parent
}
}
onDeletedFileOrDirectory = func(path tspath.Path) {
dirEntry, ok := s.diskDirectories.Get(path.GetDirectoryPath())
if !ok {
return
}
dirEntry.Change(func(dir dirty.CloneableMap[tspath.Path, string]) {
delete(dir, path)
if len(dir) == 0 {
dirEntry.Delete()
onDeletedFileOrDirectory(dirEntry.Key())
}
})
}
diskFiles, changed := s.diskFiles.FinalizeWith(dirty.FinalizationHooks[tspath.Path, *diskFile]{
OnDelete: func(key tspath.Path, value *diskFile) {
if deleted == nil {
deleted = make(map[tspath.Path]*diskFile)
}
deleted[key] = value
},
OnAdd: func(key tspath.Path, value *diskFile) {
onAddedFile(key, value.FileName())
},
})
for path := range deleted {
onDeletedFileOrDirectory(path)
}
// Prune deleted symlink paths from realpath alias sets before finalizing,
// so that empty sets are dropped during finalization.
for deletedPath, deletedFile := range deleted {
if deletedFile.realpathPath == "" {
continue
}
if entry, ok := s.nodeModulesRealpathAliases.Load(deletedFile.realpathPath); ok {
entry.Locked(func(e dirty.Value[*realpathAliasSet]) {
e.Change(func(aliasSet *realpathAliasSet) {
aliasSet.paths.Delete(deletedPath)
})
if e.Value().paths.Len() == 0 {
e.Delete()
}
})
}
}
nodeModulesRealpathAliases, aliasesChanged := s.nodeModulesRealpathAliases.Finalize()
return &SnapshotFS{
fs: s.fs,
overlays: s.overlays,
overlayDirectories: s.overlayDirectories,
diskFiles: diskFiles,
diskDirectories: core.FirstResult(s.diskDirectories.Finalize()),
nodeModulesRealpathAliases: nodeModulesRealpathAliases,
toPath: s.toPath,
}, changed || aliasesChanged
}
func (s *snapshotFSBuilder) isOpenFile(path tspath.Path) bool {
_, ok := s.overlays[path]
return ok
}
func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle {
path := s.toPath(fileName)
return s.GetFileByPath(fileName, path)
}
func (s *snapshotFSBuilder) FileExists(fileName string, path tspath.Path) bool {
if _, ok := s.overlays[path]; ok {
return true
}
if entry, ok := s.diskFiles.Load(path); ok {
val := entry.Value()
if val == nil {
return false
}
// Entry may be dirty - reload to check current state on disk.
return s.reloadEntryIfNeeded(entry) != nil
}
// Path never loaded into diskFiles - use cached stat (no file read).
return s.fs.FileExists(fileName)
}
func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) FileHandle {
if file, ok := s.overlays[path]; ok {
return file
}
return s.getDiskFile(fileName, path, false)
}
func (s *snapshotFSBuilder) GetAccessibleEntries(path string) vfs.Entries {
entries := s.fs.GetAccessibleEntries(path)
p := s.toPath(path)
overlayDirectories, ok := s.overlayDirectories[p]
if !ok {
return entries
}
if merged, ok := s.accessibleEntries.Load(p); ok {
return *merged
}
merged := &vfs.Entries{
Files: slices.Clip(entries.Files),
Directories: slices.Clip(entries.Directories),
Symlinks: entries.Symlinks,
}
readDirectoryIntoEntries(overlayDirectories, s.isOpenFile, merged)
merged, _ = s.accessibleEntries.LoadOrStore(p, merged)
return *merged
}
func (s *snapshotFSBuilder) getDiskFile(fileName string, path tspath.Path, forceReload bool) FileHandle {
entry, loaded := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true})
if entry != nil {
if !loaded && strings.Contains(string(path), "/node_modules/") {
s.recordRealpathAlias(entry, fileName, path)
}
if forceReload {
return s.reloadEntry(entry)
}
return s.reloadEntryIfNeeded(entry)
}
return nil
}
// recordRealpathAlias checks if fileName is accessed through a symlink and, if so,
// records a mapping from the realpath-based key to the symlink-based key.
// This is only called for files inside node_modules where symlinks are common.
func (s *snapshotFSBuilder) recordRealpathAlias(diskFileEntry *dirty.SyncMapEntry[tspath.Path, *diskFile], symlinkFileName string, symlinkPath tspath.Path) {
realpath := s.fs.Realpath(symlinkFileName)
realpathPath := s.toPath(realpath)
if realpathPath != symlinkPath {
diskFileEntry.Change(func(file *diskFile) {
file.realpathPath = realpathPath
})
entry, _ := s.nodeModulesRealpathAliases.LoadOrStore(realpathPath, &realpathAliasSet{})
entry.Change(func(aliasSet *realpathAliasSet) {
aliasSet.Add(symlinkPath)
})
}
}
func (s *snapshotFSBuilder) reloadEntry(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) FileHandle {
var fileName string
entry.Locked(func(e dirty.Value[*diskFile]) {
if e.Value() != nil {
fileName = e.Value().fileName
}
})
if fileName == "" {
return nil
}
// Read file outside the lock to avoid blocking other goroutines.
content, ok := s.fs.ReadFile(fileName)
entry.Locked(func(e dirty.Value[*diskFile]) {
if e.Value() == nil {
return
}
if ok {
e.Change(func(file *diskFile) {
file.content = content
file.hash = xxh3.HashString128(content)
file.needsReload = false
})
} else {
e.Delete()
}
})
if entry.Value() == nil {
return nil
}
return entry.Value()
}
func (s *snapshotFSBuilder) reloadEntryIfNeeded(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) FileHandle {
var fileName string
entry.Locked(func(e dirty.Value[*diskFile]) {
if e.Value() != nil && !e.Value().MatchesDiskText() {
fileName = e.Value().fileName
}
})
if fileName != "" {
// Read file outside the lock to avoid blocking other goroutines.
content, ok := s.fs.ReadFile(fileName)
entry.Locked(func(e dirty.Value[*diskFile]) {
if e.Value() == nil || e.Value().MatchesDiskText() {
return // another goroutine already reloaded it
}
if ok {
e.Change(func(file *diskFile) {
file.content = content
file.hash = xxh3.HashString128(content)
file.needsReload = false
})
} else {
e.Delete()
}
})
}
if entry.Value() == nil {
return nil
}
return entry.Value()
}
func (s *snapshotFSBuilder) watchChangesOverlapCache(change FileChangeSummary) bool {
for uri := range change.Changed.Keys() {
path := s.toPath(uri.FileName())
if _, ok := s.diskFiles.Load(path); ok {
return true
}
if _, ok := s.nodeModulesRealpathAliases.Load(path); ok {
return true
}
}
for uri := range change.Deleted.Keys() {
path := s.toPath(uri.FileName())
if _, ok := s.diskFiles.Load(path); ok {
return true
}
if _, ok := s.nodeModulesRealpathAliases.Load(path); ok {
return true
}
}
return false
}
func (s *snapshotFSBuilder) invalidateCache() {
s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool {
entry.Change(func(file *diskFile) {
file.needsReload = true
})
return true
})
}
func (s *snapshotFSBuilder) invalidateNodeModulesCache() {
s.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool {
if strings.Contains(string(entry.Key()), "/node_modules/") {
entry.Change(func(file *diskFile) {
file.needsReload = true
})
}
return true
})
}
func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) {
for uri := range change.Changed.Keys() {
path := s.toPath(uri.FileName())
if entry, ok := s.diskFiles.Load(path); ok {
entry.Change(func(file *diskFile) {
file.needsReload = true
})
}
}
for uri := range change.Deleted.Keys() {
path := s.toPath(uri.FileName())
if entry, ok := s.diskFiles.Load(path); ok {
entry.Delete()
}
}
}
// expandRealpathAliases adds synthetic URIs to the Changed 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 {
if len(s.nodeModulesRealpathAliases) == 0 {
return change
}
var additionalChanged collections.Set[lsproto.DocumentUri]
for uri := range change.Changed.Keys() {
path := s.toPath(uri.FileName())
if aliases, ok := s.nodeModulesRealpathAliases[path]; ok {
for aliasPath := range aliases.paths.Keys() {
additionalChanged.Add(lsconv.FileNameToDocumentURI(string(aliasPath)))
}
}
}
for uri := range additionalChanged.Keys() {
change.Changed.Add(uri)
}
var additionalDeleted collections.Set[lsproto.DocumentUri]
for uri := range change.Deleted.Keys() {
path := s.toPath(uri.FileName())
if aliases, ok := s.nodeModulesRealpathAliases[path]; ok {
for aliasPath := range aliases.paths.Keys() {
additionalDeleted.Add(lsconv.FileNameToDocumentURI(string(aliasPath)))
}
}
}
for uri := range additionalDeleted.Keys() {
change.Deleted.Add(uri)
}
return change
}
// isRelevantFileName returns true if the given URI refers to a file that
// could affect the project: it has a TypeScript-relevant extension, is a
// dynamic (e.g. untitled) file, or is currently open as an overlay.
func (s *snapshotFSBuilder) isRelevantFileName(uri lsproto.DocumentUri) bool {
fileName := uri.FileName()
if tspath.IsDynamicFileName(fileName) {
return true
}
path := s.toPath(fileName)
if _, ok := s.overlays[path]; ok {
return true
}
i := strings.LastIndexByte(string(path), '.')
if i < 0 {
return false
}
switch string(path)[i:] {
case ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json":
return true
}
return false
}
// expandAndFilterWatchEvents expands directory deletion URIs into individual
// file deletion URIs using the cached directory structure, and filters out
// watch events for paths that are neither known directories nor have relevant
// file extensions.
func (s *snapshotFSBuilder) expandAndFilterWatchEvents(change FileChangeSummary) FileChangeSummary {
if change.Deleted.Len() > 0 {
var filteredDeleted collections.Set[lsproto.DocumentUri]
for uri := range change.Deleted.Keys() {
path := s.toPath(uri.FileName())
if _, ok := s.diskDirectories.Get(path); ok {
s.collectFilesRecursive(path, &filteredDeleted)
} else if s.isRelevantFileName(uri) || isNodeModulesPath(path) {
// node_modules deletions must always be preserved for auto-import registry change handlers.
// They won't be in diskDirectories since the registry doesn't use the snapshotFSBuilder for
// its file system, since we don't want to retain files read there.
filteredDeleted.Add(uri)
}
}
change.Deleted = filteredDeleted
}
if change.Changed.Len() > 0 {
var filteredChanged collections.Set[lsproto.DocumentUri]
for uri := range change.Changed.Keys() {
if s.isRelevantFileName(uri) {
filteredChanged.Add(uri)
}
}
change.Changed = filteredChanged
}
// We can't filter created events because any created path could be a directory symlink
// that includes relevant files. configFileRegistryBuilder will do check if these paths
// are directories if they fall within a config's wildcard directories.
return change
}
// isNodeModulesPath reports whether path is a node_modules directory itself or
// lives inside one. Used to preserve node_modules watch deletions, whose package
// files are read transiently and therefore never tracked in diskDirectories.
func isNodeModulesPath(path tspath.Path) bool {
s := string(path)
return strings.HasSuffix(s, "/node_modules") || strings.Contains(s, "/node_modules/")
}
// collectFilesRecursive recursively collects all cached file URIs under the
// given directory path using the diskDirectories and diskFiles maps.
func (s *snapshotFSBuilder) collectFilesRecursive(dirPath tspath.Path, files *collections.Set[lsproto.DocumentUri]) {
dirEntry, ok := s.diskDirectories.Get(dirPath)
if !ok {
return
}
for childPath := range dirEntry.Value() {
if entry, ok := s.diskFiles.Load(childPath); ok {
if file := entry.Value(); file != nil {
files.Add(lsconv.FileNameToDocumentURI(file.FileName()))
}
}
s.collectFilesRecursive(childPath, files)
}
}
func (s *snapshotFSBuilder) convertOpenAndCloseToChanges(change FileChangeSummary) FileChangeSummary {
if change.Opened != "" && !tspath.IsDynamicFileName(change.Opened.FileName()) {
path := s.toPath(change.Opened.FileName())
if entry, ok := s.diskFiles.Load(path); !ok || entry.Original() == nil {
change.Created.Add(change.Opened)
} else if overlay, ok := s.overlays[path]; ok {
// The file already exists in the program, but the overlay content from
// didOpen may differ from what was originally read from disk (e.g. the
// editor normalizes line endings, or the file changed on disk since the
// project was loaded). Mark it as Changed so the project rebuilds.
if diskFile := entry.Original(); diskFile != nil && overlay.Hash() != diskFile.Hash() {
change.Changed.Add(change.Opened)
}
}
}
for uri := range change.Closed.Keys() {
fileName := uri.FileName()
if tspath.IsDynamicFileName(fileName) {
continue
}
path := s.toPath(fileName)
// We may have ignored watcher events while the file was open, so force a reload.
if fh := s.getDiskFile(fileName, path, true /*forceReload*/); fh != nil {
if fh.Hash() != s.prevOverlays[path].Hash() {
change.Changed.Add(uri)
}
continue
}
change.Deleted.Add(uri)
}
return change
}
// sourceFS is a vfs.FS that sources files from a FileSource and tracks seen files.
type sourceFS struct {
tracking bool
toPath func(fileName string) tspath.Path
missingDirectories *collections.SyncSet[tspath.Path]
seenFiles *collections.SyncSet[tspath.Path]
source FileSource
}
func newSourceFS(tracking bool, source FileSource, toPath func(fileName string) tspath.Path) *sourceFS {
fs := &sourceFS{
tracking: tracking,
toPath: toPath,
source: source,
}
if tracking {
fs.seenFiles = &collections.SyncSet[tspath.Path]{}
fs.missingDirectories = &collections.SyncSet[tspath.Path]{}
}
return fs
}
var _ vfs.FS = (*sourceFS)(nil)
func (fs *sourceFS) DisableTracking() {
fs.tracking = false
}
func (fs *sourceFS) Track(fileName string) {
if !fs.tracking {
return
}
fs.seenFiles.Add(fs.toPath(fileName))
}
func (fs *sourceFS) SeenFile(path tspath.Path) bool {
if fs.seenFiles == nil {
return false
}
return fs.seenFiles.Has(path)
}
func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.Path) bool {
if fs.seenFiles != nil && fs.seenFiles.Has(path) {
return true
}
if fs.missingDirectories != nil && !fs.missingDirectories.IsEmpty() {
for {
if fs.missingDirectories.Has(path) {
return true
}
parent := path.GetDirectoryPath()
if parent == path {
break
}
path = parent
}
}
return false
}
func (fs *sourceFS) GetFile(fileName string) FileHandle {
fs.Track(fileName)
return fs.source.GetFile(fileName)
}
func (fs *sourceFS) GetFileByPath(fileName string, path tspath.Path) FileHandle {
fs.Track(fileName)
return fs.source.GetFileByPath(fileName, path)
}
// DirectoryExists implements vfs.FS.
func (fs *sourceFS) DirectoryExists(path string) bool {
exists := fs.source.FS().DirectoryExists(path)
if !exists && fs.tracking {
fs.missingDirectories.Add(fs.toPath(path))
}
return exists
}
// FileExists implements vfs.FS.
func (fs *sourceFS) FileExists(path string) bool {
fs.Track(path)
return fs.source.FileExists(path, fs.toPath(path))
}
// GetAccessibleEntries implements vfs.FS.
func (fs *sourceFS) GetAccessibleEntries(path string) vfs.Entries {
return fs.source.GetAccessibleEntries(path)
}
// ReadFile implements vfs.FS.
func (fs *sourceFS) ReadFile(path string) (contents string, ok bool) {
if fh := fs.GetFile(path); fh != nil {
return fh.Content(), true
}
return "", false
}
// Realpath implements vfs.FS.
func (fs *sourceFS) Realpath(path string) string {
return fs.source.FS().Realpath(path)
}
// Stat implements vfs.FS.
func (fs *sourceFS) Stat(path string) vfs.FileInfo {
return fs.source.FS().Stat(path)
}
// UseCaseSensitiveFileNames implements vfs.FS.
func (fs *sourceFS) UseCaseSensitiveFileNames() bool {
return fs.source.FS().UseCaseSensitiveFileNames()
}
// WalkDir implements vfs.FS.
func (fs *sourceFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
return fs.source.FS().WalkDir(root, walkFn)
}
// WriteFile implements vfs.FS.
func (fs *sourceFS) WriteFile(path string, data string) error {
panic("unimplemented")
}
// AppendFile implements vfs.FS.
func (fs *sourceFS) AppendFile(path string, data string) error {
panic("unimplemented")
}
// Remove implements vfs.FS.
func (fs *sourceFS) Remove(path string) error {
panic("unimplemented")
}
// Chtimes implements vfs.FS.
func (fs *sourceFS) Chtimes(path string, atime time.Time, mtime time.Time) error {
panic("unimplemented")
}
func readDirectoryIntoEntries[M ~map[tspath.Path]string](directories M, isFile func(tspath.Path) bool, entries *vfs.Entries) {
for childPath, childName := range directories {
if isFile(childPath) {
entries.Files = append(entries.Files, childName)
} else {
entries.Directories = append(entries.Directories, childName)
}
}
}