diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 731a11e891e..985011feef9 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -1074,7 +1074,7 @@ func (s *scanner) addEntryPoints(entryPoints []string) []uint32 { dir := s.fs.Dir(absPath) base := s.fs.Base(absPath) if entries, err := s.fs.ReadDirectory(dir); err == nil { - if entry := entries[base]; entry != nil && entry.Kind() == fs.FileEntry { + if entry := entries[base]; entry != nil && entry.Kind(s.fs) == fs.FileEntry { entryPoints[i] = "./" + path } } diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 60328639d48..4c20262a872 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -2,8 +2,6 @@ package fs import ( "errors" - "os" - "path/filepath" "sync" ) @@ -23,67 +21,26 @@ type Entry struct { needStat bool } -func (e *Entry) Kind() EntryKind { +func (e *Entry) Kind(fs FS) EntryKind { e.mutex.Lock() defer e.mutex.Unlock() if e.needStat { - e.stat() + e.needStat = false + e.symlink, e.kind = fs.kind(e.dir, e.base) } return e.kind } -func (e *Entry) Symlink() string { +func (e *Entry) Symlink(fs FS) string { e.mutex.Lock() defer e.mutex.Unlock() if e.needStat { - e.stat() + e.needStat = false + e.symlink, e.kind = fs.kind(e.dir, e.base) } return e.symlink } -func (e *Entry) stat() { - e.needStat = false - entryPath := filepath.Join(e.dir, e.base) - - // Use "lstat" since we want information about symbolic links - BeforeFileOpen() - defer AfterFileClose() - stat, err := os.Lstat(entryPath) - if err != nil { - return - } - mode := stat.Mode() - - // Follow symlinks now so the cache contains the translation - if (mode & os.ModeSymlink) != 0 { - link, err := os.Readlink(entryPath) - if err != nil { - return // Skip over this entry - } - if !filepath.IsAbs(link) { - link = filepath.Join(e.dir, link) - } - e.symlink = filepath.Clean(link) - - // Re-run "lstat" on the symlink target - stat2, err2 := os.Lstat(e.symlink) - if err2 != nil { - return // Skip over this entry - } - mode = stat2.Mode() - if (mode & os.ModeSymlink) != 0 { - return // Symlink chains are not supported - } - } - - // We consider the entry either a directory or a file - if (mode & os.ModeDir) != 0 { - e.kind = DirEntry - } else { - e.kind = FileEntry - } -} - type FS interface { // The returned map is immutable and is cached across invocations. Do not // mutate it. @@ -114,6 +71,9 @@ type FS interface { Join(parts ...string) string Cwd() string Rel(base string, target string) (string, bool) + + // This is used in the implementation of "Entry" + kind(dir string, base string) (symlink string, kind EntryKind) } type ModKey struct { diff --git a/internal/fs/fs_mock.go b/internal/fs/fs_mock.go index 4489c7d9362..7ec1242c7bf 100644 --- a/internal/fs/fs_mock.go +++ b/internal/fs/fs_mock.go @@ -138,3 +138,8 @@ func (*mockFS) Rel(base string, target string) (string, bool) { // Otherwise, down to the parent return commonParent + target, true } + +func (fs *mockFS) kind(dir string, base string) (symlink string, kind EntryKind) { + // This will never be called + return +} diff --git a/internal/fs/fs_mock_test.go b/internal/fs/fs_mock_test.go index 2c547568a26..3be61a67924 100644 --- a/internal/fs/fs_mock_test.go +++ b/internal/fs/fs_mock_test.go @@ -48,7 +48,7 @@ func TestMockFSBasic(t *testing.T) { if err != nil { t.Fatal("Expected to find /src") } - if len(src) != 2 || src["index.js"].Kind() != FileEntry || src["util.js"].Kind() != FileEntry { + if len(src) != 2 || src["index.js"].Kind(fs) != FileEntry || src["util.js"].Kind(fs) != FileEntry { t.Fatalf("Incorrect contents for /src: %v", src) } @@ -57,7 +57,7 @@ func TestMockFSBasic(t *testing.T) { if err != nil { t.Fatal("Expected to find /") } - if len(slash) != 3 || slash["src"].Kind() != DirEntry || slash["README.md"].Kind() != FileEntry || slash["package.json"].Kind() != FileEntry { + if len(slash) != 3 || slash["src"].Kind(fs) != DirEntry || slash["README.md"].Kind(fs) != FileEntry || slash["package.json"].Kind(fs) != FileEntry { t.Fatalf("Incorrect contents for /: %v", slash) } } diff --git a/internal/fs/fs_real.go b/internal/fs/fs_real.go index 9846740e742..b5c13d400ae 100644 --- a/internal/fs/fs_real.go +++ b/internal/fs/fs_real.go @@ -182,3 +182,46 @@ func readdir(dirname string) ([]string, error) { return entries, err } + +func (fs *realFS) kind(dir string, base string) (symlink string, kind EntryKind) { + entryPath := filepath.Join(dir, base) + + // Use "lstat" since we want information about symbolic links + BeforeFileOpen() + defer AfterFileClose() + stat, err := os.Lstat(entryPath) + if err != nil { + return + } + mode := stat.Mode() + + // Follow symlinks now so the cache contains the translation + if (mode & os.ModeSymlink) != 0 { + link, err := os.Readlink(entryPath) + if err != nil { + return // Skip over this entry + } + if !filepath.IsAbs(link) { + link = filepath.Join(dir, link) + } + symlink = filepath.Clean(link) + + // Re-run "lstat" on the symlink target + stat2, err2 := os.Lstat(symlink) + if err2 != nil { + return // Skip over this entry + } + mode = stat2.Mode() + if (mode & os.ModeSymlink) != 0 { + return // Symlink chains are not supported + } + } + + // We consider the entry either a directory or a file + if (mode & os.ModeDir) != 0 { + kind = DirEntry + } else { + kind = FileEntry + } + return +} diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 86008134ec9..56da265df39 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -334,7 +334,7 @@ func (r *resolver) finalizeResolve(result ResolveResult) *ResolveResult { } if entry, ok := dirInfo.entries[base]; ok { - if symlink := entry.Symlink(); symlink != "" { + if symlink := entry.Symlink(r.fs); symlink != "" { // Is this entry itself a symlink? path.Text = symlink } else if dirInfo.absRealPath != "" { @@ -726,7 +726,7 @@ func (r *resolver) dirInfoUncached(path string) *dirInfo { base := r.fs.Base(path) if base != "node_modules" { if entry, ok := entries["node_modules"]; ok { - info.hasNodeModules = entry.Kind() == fs.DirEntry + info.hasNodeModules = entry.Kind(r.fs) == fs.DirEntry } } @@ -736,7 +736,7 @@ func (r *resolver) dirInfoUncached(path string) *dirInfo { // Make sure "absRealPath" is the real path of the directory (resolving any symlinks) if entry, ok := parentInfo.entries[base]; ok { - if symlink := entry.Symlink(); symlink != "" { + if symlink := entry.Symlink(r.fs); symlink != "" { info.absRealPath = symlink } else if parentInfo.absRealPath != "" { info.absRealPath = r.fs.Join(parentInfo.absRealPath, base) @@ -745,7 +745,7 @@ func (r *resolver) dirInfoUncached(path string) *dirInfo { } // Record if this directory has a package.json file - if entry, ok := entries["package.json"]; ok && entry.Kind() == fs.FileEntry { + if entry, ok := entries["package.json"]; ok && entry.Kind(r.fs) == fs.FileEntry { info.packageJSON = r.parsePackageJSON(path) // Propagate this browser scope into child directories @@ -758,9 +758,9 @@ func (r *resolver) dirInfoUncached(path string) *dirInfo { { var tsConfigPath string if forceTsConfig := r.options.TsConfigOverride; forceTsConfig == "" { - if entry, ok := entries["tsconfig.json"]; ok && entry.Kind() == fs.FileEntry { + if entry, ok := entries["tsconfig.json"]; ok && entry.Kind(r.fs) == fs.FileEntry { tsConfigPath = r.fs.Join(path, "tsconfig.json") - } else if entry, ok := entries["jsconfig.json"]; ok && entry.Kind() == fs.FileEntry { + } else if entry, ok := entries["jsconfig.json"]; ok && entry.Kind(r.fs) == fs.FileEntry { tsConfigPath = r.fs.Join(path, "jsconfig.json") } } else if parentInfo == nil { @@ -1007,13 +1007,13 @@ func (r *resolver) loadAsFile(path string, extensionOrder []string) (string, boo base := r.fs.Base(path) // Try the plain path without any extensions - if entry, ok := entries[base]; ok && entry.Kind() == fs.FileEntry { + if entry, ok := entries[base]; ok && entry.Kind(r.fs) == fs.FileEntry { return path, true } // Try the path with extensions for _, ext := range extensionOrder { - if entry, ok := entries[base+ext]; ok && entry.Kind() == fs.FileEntry { + if entry, ok := entries[base+ext]; ok && entry.Kind(r.fs) == fs.FileEntry { return path + ext, true } } @@ -1038,7 +1038,7 @@ func (r *resolver) loadAsFile(path string, extensionOrder []string) (string, boo // Note that the official compiler code always tries ".ts" before // ".tsx" even if the original extension was ".jsx". for _, ext := range []string{".ts", ".tsx"} { - if entry, ok := entries[base[:lastDot]+ext]; ok && entry.Kind() == fs.FileEntry { + if entry, ok := entries[base[:lastDot]+ext]; ok && entry.Kind(r.fs) == fs.FileEntry { return path[:len(path)-(len(base)-lastDot)] + ext, true } } @@ -1054,7 +1054,7 @@ func (r *resolver) loadAsIndex(path string, entries map[string]*fs.Entry) (strin // Try the "index" file with extensions for _, ext := range r.options.ExtensionOrder { base := "index" + ext - if entry, ok := entries[base]; ok && entry.Kind() == fs.FileEntry { + if entry, ok := entries[base]; ok && entry.Kind(r.fs) == fs.FileEntry { return r.fs.Join(path, base), true } }