vendor tsgo
This commit is contained in:
27
tools/tsgo/internal/vfs/osvfs/helpers_test.go
Normal file
27
tools/tsgo/internal/vfs/osvfs/helpers_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package osvfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func mklink(tb testing.TB, target, link string, isDir bool) {
|
||||
tb.Helper()
|
||||
|
||||
if runtime.GOOS == "windows" && isDir {
|
||||
// Don't use os.Symlink on Windows, as it creates a "real" symlink, not a junction.
|
||||
assert.NilError(tb, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run())
|
||||
} else {
|
||||
err := os.Symlink(target, link)
|
||||
if err != nil && !isDir && runtime.GOOS == "windows" && strings.Contains(err.Error(), "A required privilege is not held by the client") {
|
||||
tb.Log(err)
|
||||
tb.Skip("file symlink support is not enabled without elevation or developer mode")
|
||||
}
|
||||
assert.NilError(tb, err)
|
||||
}
|
||||
}
|
||||
242
tools/tsgo/internal/vfs/osvfs/os.go
Normal file
242
tools/tsgo/internal/vfs/osvfs/os.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package osvfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/nativepath"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
// Semaphore for operations that are effectively blocking syscalls.
|
||||
blockingOpSema = core.NewLimitedSemaphore(128)
|
||||
// Semaphore for file reads.
|
||||
readSema = core.NewLimitedSemaphore(128)
|
||||
// Semaphore for file writes.
|
||||
writeSema = core.NewLimitedSemaphore(32)
|
||||
)
|
||||
|
||||
// FS creates a new FS from the OS file system.
|
||||
func FS() vfs.FS {
|
||||
return osVFS
|
||||
}
|
||||
|
||||
var osVFS vfs.FS = &osFS{
|
||||
common: internal.Common{
|
||||
RootFor: os.DirFS,
|
||||
IsReparsePoint: isReparsePoint,
|
||||
},
|
||||
}
|
||||
|
||||
type osFS struct {
|
||||
common internal.Common
|
||||
}
|
||||
|
||||
// We do this right at startup to minimize the chance that executable gets moved or deleted.
|
||||
var isFileSystemCaseSensitive = func() bool {
|
||||
// win32/win64 are case insensitive platforms
|
||||
if runtime.GOOS == "windows" {
|
||||
return false
|
||||
}
|
||||
|
||||
if runtime.GOARCH == "wasm" {
|
||||
// !!! Who knows; this depends on the host implementation.
|
||||
return true
|
||||
}
|
||||
|
||||
// As a proxy for case-insensitivity, we check if the current executable exists under a different case.
|
||||
// This is not entirely correct, since different OSs can have differing case sensitivity in different paths,
|
||||
// but this is largely good enough for our purposes (and what sys.ts used to do with __filename).
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vfs: failed to get executable path: %v", err))
|
||||
}
|
||||
|
||||
// If the current executable exists under a different case, we must be case-insensitive.
|
||||
swapped := swapCase(exe)
|
||||
if _, err := os.Stat(swapped); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
panic(fmt.Sprintf("vfs: failed to stat %q: %v", swapped, err))
|
||||
}
|
||||
return false
|
||||
}()
|
||||
|
||||
// Convert all lowercase chars to uppercase, and vice-versa
|
||||
func swapCase(str string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
upper := unicode.ToUpper(r)
|
||||
if upper == r {
|
||||
return unicode.ToLower(r)
|
||||
} else {
|
||||
return upper
|
||||
}
|
||||
}, str)
|
||||
}
|
||||
|
||||
func (vfs *osFS) UseCaseSensitiveFileNames() bool {
|
||||
return isFileSystemCaseSensitive
|
||||
}
|
||||
|
||||
func (vfs *osFS) ReadFile(path string) (contents string, ok bool) {
|
||||
defer readSema.Acquire()()
|
||||
return vfs.common.ReadFile(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) DirectoryExists(path string) bool {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.DirectoryExists(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) FileExists(path string) bool {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.FileExists(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) GetAccessibleEntries(path string) vfs.Entries {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.GetAccessibleEntries(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Stat(path string) vfs.FileInfo {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.Stat(path)
|
||||
}
|
||||
|
||||
var limitedWalkDirFuncPool = sync.Pool{
|
||||
New: func() any {
|
||||
w := &limitedWalkDirFunc{}
|
||||
w.walk = w.walker
|
||||
return w
|
||||
},
|
||||
}
|
||||
|
||||
func getLimitedWalkDirFunc(walkFn vfs.WalkDirFunc) *limitedWalkDirFunc {
|
||||
w := limitedWalkDirFuncPool.Get().(*limitedWalkDirFunc)
|
||||
w.inner = walkFn
|
||||
return w
|
||||
}
|
||||
|
||||
func putLimitedWalkDirFunc(w *limitedWalkDirFunc) {
|
||||
w.inner = nil
|
||||
limitedWalkDirFuncPool.Put(w)
|
||||
}
|
||||
|
||||
type limitedWalkDirFunc struct {
|
||||
inner vfs.WalkDirFunc
|
||||
walk vfs.WalkDirFunc
|
||||
}
|
||||
|
||||
func (w *limitedWalkDirFunc) walker(path string, d fs.DirEntry, err error) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return w.inner(path, d, err)
|
||||
}
|
||||
|
||||
func (vfs *osFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
walker := getLimitedWalkDirFunc(walkFn)
|
||||
defer putLimitedWalkDirFunc(walker)
|
||||
return vfs.common.WalkDir(root, walker.walk)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Realpath(path string) string {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return osFSRealpath(path)
|
||||
}
|
||||
|
||||
func osFSRealpath(path string) string {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
|
||||
orig := path
|
||||
path = filepath.FromSlash(path)
|
||||
path, err := nativepath.Realpath(path)
|
||||
if err != nil {
|
||||
return orig
|
||||
}
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return orig
|
||||
}
|
||||
return tspath.NormalizeSlashes(path)
|
||||
}
|
||||
|
||||
func isReparsePoint(path string) bool {
|
||||
return nativepath.IsSymlinkOrReparsePoint(filepath.FromSlash(path))
|
||||
}
|
||||
|
||||
func (vfs *osFS) writeFileWithFlag(path string, content string, flag int) error {
|
||||
defer writeSema.Acquire()()
|
||||
|
||||
file, err := os.OpenFile(path, flag, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vfs *osFS) ensureDirectoryExists(directoryPath string) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return os.MkdirAll(directoryPath, 0o777)
|
||||
}
|
||||
|
||||
func (vfs *osFS) writeFileEnsuringDir(path string, content string, flag int) error {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
if err := vfs.writeFileWithFlag(path, content, flag); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := vfs.ensureDirectoryExists(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil {
|
||||
return err
|
||||
}
|
||||
return vfs.writeFileWithFlag(path, content, flag)
|
||||
}
|
||||
|
||||
func (vfs *osFS) WriteFile(path string, content string) error {
|
||||
return vfs.writeFileEnsuringDir(path, content, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
}
|
||||
|
||||
func (vfs *osFS) AppendFile(path string, content string) error {
|
||||
return vfs.writeFileEnsuringDir(path, content, os.O_WRONLY|os.O_CREATE|os.O_APPEND)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Remove(path string) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
// todo: #701 add retry mechanism?
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return os.Chtimes(path, aTime, mTime)
|
||||
}
|
||||
|
||||
func GetGlobalTypingsCacheLocation() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
cacheDir = os.TempDir()
|
||||
}
|
||||
|
||||
var subdir string
|
||||
if runtime.GOOS == "windows" {
|
||||
subdir = "Microsoft/TypeScript"
|
||||
} else {
|
||||
subdir = "typescript"
|
||||
}
|
||||
return tspath.CombinePaths(cacheDir, subdir, core.VersionMajorMinor())
|
||||
}
|
||||
67
tools/tsgo/internal/vfs/osvfs/os_test.go
Normal file
67
tools/tsgo/internal/vfs/osvfs/os_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package osvfs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestOS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := osvfs.FS()
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
goMod := filepath.Join(repo.RootPath(), "go.mod")
|
||||
goModPath := tspath.NormalizePath(goMod)
|
||||
|
||||
expectedRaw, err := os.ReadFile(goMod)
|
||||
assert.NilError(t, err)
|
||||
expected := string(expectedRaw)
|
||||
|
||||
contents, ok := fs.ReadFile(goModPath)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, contents, expected)
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
home = tspath.NormalizePath(home)
|
||||
|
||||
expected := home
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows drive letters can be lowercase, but realpath will always return uppercase.
|
||||
expected = strings.ToUpper(expected[:1]) + expected[1:]
|
||||
}
|
||||
realpath := fs.Realpath(home)
|
||||
assert.Equal(t, realpath, expected)
|
||||
})
|
||||
|
||||
t.Run("UseCaseSensitiveFileNames", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Just check that it works.
|
||||
fs.UseCaseSensitiveFileNames()
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
assert.Assert(t, !fs.UseCaseSensitiveFileNames())
|
||||
case "linux":
|
||||
assert.Assert(t, fs.UseCaseSensitiveFileNames())
|
||||
}
|
||||
})
|
||||
}
|
||||
152
tools/tsgo/internal/vfs/osvfs/realpath_test.go
Normal file
152
tools/tsgo/internal/vfs/osvfs/realpath_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package osvfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestSymlinkRealpath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
targetFile, linkFile := setupSymlinks(t)
|
||||
|
||||
gotContents, err := os.ReadFile(linkFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(gotContents), "hello")
|
||||
|
||||
fs := FS()
|
||||
|
||||
targetRealpath := fs.Realpath(tspath.NormalizePath(targetFile))
|
||||
linkRealpath := fs.Realpath(tspath.NormalizePath(linkFile))
|
||||
|
||||
if targetRealpath != linkRealpath {
|
||||
t.Errorf("expected realpath of target and link to be equal, got %q and %q", targetRealpath, linkRealpath)
|
||||
cmd := exec.Command("node", "-e", `console.log({ native: fs.realpathSync.native(process.argv[1]), node: fs.realpathSync(process.argv[1]) })`, linkFile)
|
||||
out, err := cmd.CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
t.Logf("node: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func setupSymlinks(tb testing.TB) (targetFile, linkFile string) {
|
||||
tb.Helper()
|
||||
|
||||
tmp := tb.TempDir()
|
||||
|
||||
target := filepath.Join(tmp, "target")
|
||||
targetFile = filepath.Join(target, "file")
|
||||
|
||||
link := filepath.Join(tmp, "link")
|
||||
linkFile = filepath.Join(link, "file")
|
||||
|
||||
assert.NilError(tb, os.MkdirAll(target, 0o777))
|
||||
assert.NilError(tb, os.WriteFile(targetFile, []byte("hello"), 0o666))
|
||||
|
||||
mklink(tb, target, link, true)
|
||||
|
||||
return targetFile, linkFile
|
||||
}
|
||||
|
||||
func BenchmarkRealpath(b *testing.B) {
|
||||
targetFile, linkFile := setupSymlinks(b)
|
||||
|
||||
fs := FS()
|
||||
normalizedTargetFile := tspath.NormalizePath(targetFile)
|
||||
normalizedLinkFile := tspath.NormalizePath(linkFile)
|
||||
|
||||
b.Run("target", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
fs.Realpath(normalizedTargetFile)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("link", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
fs.Realpath(normalizedLinkFile)
|
||||
}
|
||||
})
|
||||
|
||||
// Simulate a deep node_modules path to show scaling with depth.
|
||||
deepDir := b.TempDir()
|
||||
for _, seg := range []string{"project", "node_modules", "@scope", "package", "node_modules", "dep", "lib", "dist", "esm", "internal", "utils"} {
|
||||
deepDir = filepath.Join(deepDir, seg)
|
||||
}
|
||||
assert.NilError(b, os.MkdirAll(deepDir, 0o777))
|
||||
deepFile := filepath.Join(deepDir, "index.js")
|
||||
assert.NilError(b, os.WriteFile(deepFile, []byte("module.exports = {}"), 0o666))
|
||||
normalizedDeepFile := tspath.NormalizePath(deepFile)
|
||||
|
||||
b.Run("deep", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
fs.Realpath(normalizedDeepFile)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("deep_evalSymlinks", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
deepNative := filepath.FromSlash(normalizedDeepFile)
|
||||
|
||||
for b.Loop() {
|
||||
filepath.EvalSymlinks(deepNative) //nolint:errcheck
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAccessibleEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
target := filepath.Join(tmp, "target")
|
||||
link := filepath.Join(tmp, "link")
|
||||
|
||||
assert.NilError(t, os.MkdirAll(target, 0o777))
|
||||
assert.NilError(t, os.MkdirAll(link, 0o777))
|
||||
|
||||
targetFile1 := filepath.Join(target, "file1")
|
||||
targetFile2 := filepath.Join(target, "file2")
|
||||
|
||||
assert.NilError(t, os.WriteFile(targetFile1, []byte("hello"), 0o666))
|
||||
assert.NilError(t, os.WriteFile(targetFile2, []byte("world"), 0o666))
|
||||
|
||||
targetDir1 := filepath.Join(target, "dir1")
|
||||
targetDir2 := filepath.Join(target, "dir2")
|
||||
|
||||
assert.NilError(t, os.MkdirAll(targetDir1, 0o777))
|
||||
assert.NilError(t, os.MkdirAll(targetDir2, 0o777))
|
||||
|
||||
mklink(t, targetFile1, filepath.Join(link, "file1"), false)
|
||||
mklink(t, targetFile2, filepath.Join(link, "file2"), false)
|
||||
mklink(t, targetDir1, filepath.Join(link, "dir1"), true)
|
||||
mklink(t, targetDir2, filepath.Join(link, "dir2"), true)
|
||||
|
||||
fs := FS()
|
||||
|
||||
entries := fs.GetAccessibleEntries(tspath.NormalizePath(link))
|
||||
|
||||
assert.DeepEqual(t, entries.Directories, []string{"dir1", "dir2"})
|
||||
assert.DeepEqual(t, entries.Files, []string{"file1", "file2"})
|
||||
assert.Check(t, entries.Symlinks != nil, "expected Symlinks to be set for directory with symlinks")
|
||||
assert.Equal(t, len(entries.Symlinks), 4)
|
||||
for _, name := range []string{"file1", "file2", "dir1", "dir2"} {
|
||||
_, ok := entries.Symlinks[name]
|
||||
assert.Check(t, ok, "expected %q to be in Symlinks", name)
|
||||
}
|
||||
|
||||
// Non-symlink directory should have empty Symlinks.
|
||||
entries = fs.GetAccessibleEntries(tspath.NormalizePath(target))
|
||||
assert.DeepEqual(t, entries.Directories, []string{"dir1", "dir2"})
|
||||
assert.DeepEqual(t, entries.Files, []string{"file1", "file2"})
|
||||
assert.Check(t, entries.Symlinks != nil, "expected Symlinks to be non-nil for directory without symlinks")
|
||||
assert.Equal(t, len(entries.Symlinks), 0)
|
||||
}
|
||||
Reference in New Issue
Block a user