package iovfs import ( "fmt" "io/fs" "strings" "time" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/internal" ) type RealpathFS interface { fs.FS Realpath(path string) (string, error) } type WritableFS interface { fs.FS WriteFile(path string, data string, perm fs.FileMode) error AppendFile(path string, data string, perm fs.FileMode) error MkdirAll(path string, perm fs.FileMode) error // Removes `path` and all its contents. Will return the first error it encounters. Remove(path string) error Chtimes(path string, aTime time.Time, mTime time.Time) error } type FsWithSys interface { vfs.FS FSys() fs.FS } // From creates a new FS from an [fs.FS]. // // For paths like `c:/foo/bar`, fsys will be used as though it's rooted at `/` and the path is `/c:/foo/bar`. // // If the provided [fs.FS] implements [RealpathFS], it will be used to implement the Realpath method. // If the provided [fs.FS] implements [WritableFS], it will be used to implement the WriteFile method. // // From does not actually handle case-insensitivity; ensure the passed in [fs.FS] // respects case-insensitive file names if needed. Consider using [vfstest.FromMap] for testing. func From(fsys fs.FS, useCaseSensitiveFileNames bool) FsWithSys { var realpath func(path string) (string, error) if fsys, ok := fsys.(RealpathFS); ok { realpath = func(path string) (string, error) { rest, hadSlash := strings.CutPrefix(path, "/") rp, err := fsys.Realpath(rest) if err != nil { return "", err } if hadSlash { return "/" + rp, nil } return rp, nil } } else { realpath = func(path string) (string, error) { return path, nil } } var writeFile func(path string, content string) error var appendFile func(path string, content string) error var mkdirAll func(path string) error var remove func(path string) error var chtimes func(path string, aTime time.Time, mTime time.Time) error if fsys, ok := fsys.(WritableFS); ok { writeFile = func(path string, content string) error { rest, _ := strings.CutPrefix(path, "/") return fsys.WriteFile(rest, content, 0o666) } appendFile = func(path string, content string) error { rest, _ := strings.CutPrefix(path, "/") return fsys.AppendFile(rest, content, 0o666) } mkdirAll = func(path string) error { rest, _ := strings.CutPrefix(path, "/") return fsys.MkdirAll(rest, 0o777) } remove = func(path string) error { rest, _ := strings.CutPrefix(path, "/") return fsys.Remove(rest) } chtimes = func(path string, aTime time.Time, mTime time.Time) error { rest, _ := strings.CutPrefix(path, "/") return fsys.Chtimes(rest, aTime, mTime) } } else { writeFile = func(string, string) error { panic("writeFile not supported") } appendFile = func(string, string) error { panic("appendFile not supported") } mkdirAll = func(string) error { panic("mkdirAll not supported") } remove = func(string) error { panic("remove not supported") } chtimes = func(string, time.Time, time.Time) error { panic("chtimes not supported") } } return &ioFS{ common: internal.Common{ RootFor: func(root string) fs.FS { if root == "/" { return fsys } p := tspath.RemoveTrailingDirectorySeparator(root) sub, err := fs.Sub(fsys, p) if err != nil { if tspath.IsUrl(root) { return nil } panic(fmt.Sprintf("vfs: failed to create sub file system for %q: %v", p, err)) } return sub }, }, useCaseSensitiveFileNames: useCaseSensitiveFileNames, realpath: realpath, writeFile: writeFile, appendFile: appendFile, mkdirAll: mkdirAll, remove: remove, chtimes: chtimes, fsys: fsys, } } type ioFS struct { common internal.Common useCaseSensitiveFileNames bool realpath func(path string) (string, error) writeFile func(path string, content string) error appendFile func(path string, content string) error mkdirAll func(path string) error remove func(path string) error chtimes func(path string, aTime time.Time, mTime time.Time) error fsys fs.FS } var _ FsWithSys = (*ioFS)(nil) func (vfs *ioFS) UseCaseSensitiveFileNames() bool { return vfs.useCaseSensitiveFileNames } func (vfs *ioFS) DirectoryExists(path string) bool { return vfs.common.DirectoryExists(path) } func (vfs *ioFS) FileExists(path string) bool { return vfs.common.FileExists(path) } func (vfs *ioFS) GetAccessibleEntries(path string) vfs.Entries { return vfs.common.GetAccessibleEntries(path) } func (vfs *ioFS) Stat(path string) vfs.FileInfo { _ = internal.RootLength(path) // Assert path is rooted return vfs.common.Stat(path) } func (vfs *ioFS) ReadFile(path string) (contents string, ok bool) { return vfs.common.ReadFile(path) } func (vfs *ioFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { return vfs.common.WalkDir(root, walkFn) } func (vfs *ioFS) Remove(path string) error { _ = internal.RootLength(path) // Assert path is rooted return vfs.remove(path) } func (vfs *ioFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { _ = internal.RootLength(path) // Assert path is rooted return vfs.chtimes(path, aTime, mTime) } func (vfs *ioFS) Realpath(path string) string { root, rest := internal.SplitPath(path) // splitPath normalizes the path into parts (e.g. "c:/foo/bar" -> "c:/", "foo/bar") // Put them back together to call realpath. realpath, err := vfs.realpath(root + rest) if err != nil { return path } return realpath } func (vfs *ioFS) writeFileEnsuringDir(path string, content string, write func(path, content string) error) error { _ = internal.RootLength(path) // Assert path is rooted if err := write(path, content); err == nil { return nil } if err := vfs.mkdirAll(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil { return err } return write(path, content) } func (vfs *ioFS) WriteFile(path string, content string) error { return vfs.writeFileEnsuringDir(path, content, vfs.writeFile) } func (vfs *ioFS) AppendFile(path string, content string) error { return vfs.writeFileEnsuringDir(path, content, vfs.appendFile) } func (vfs *ioFS) FSys() fs.FS { return vfs.fsys }