github.com/evanw/esbuild@v0.21.4/internal/fs/fs.go (about)

     1  package fs
     2  
     3  // Most of esbuild's internals use this file system abstraction instead of
     4  // using native file system APIs. This lets us easily mock the file system
     5  // for tests and also implement Yarn's virtual ".zip" file system overlay.
     6  
     7  import (
     8  	"errors"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  	"syscall"
    14  )
    15  
    16  type EntryKind uint8
    17  
    18  const (
    19  	DirEntry  EntryKind = 1
    20  	FileEntry EntryKind = 2
    21  )
    22  
    23  type Entry struct {
    24  	symlink  string
    25  	dir      string
    26  	base     string
    27  	mutex    sync.Mutex
    28  	kind     EntryKind
    29  	needStat bool
    30  }
    31  
    32  func (e *Entry) Kind(fs FS) EntryKind {
    33  	e.mutex.Lock()
    34  	defer e.mutex.Unlock()
    35  	if e.needStat {
    36  		e.needStat = false
    37  		e.symlink, e.kind = fs.kind(e.dir, e.base)
    38  	}
    39  	return e.kind
    40  }
    41  
    42  func (e *Entry) Symlink(fs FS) string {
    43  	e.mutex.Lock()
    44  	defer e.mutex.Unlock()
    45  	if e.needStat {
    46  		e.needStat = false
    47  		e.symlink, e.kind = fs.kind(e.dir, e.base)
    48  	}
    49  	return e.symlink
    50  }
    51  
    52  type accessedEntries struct {
    53  	wasPresent map[string]bool
    54  
    55  	// If this is nil, "SortedKeys()" was not accessed. This means we should
    56  	// check for whether this directory has changed or not by seeing if any of
    57  	// the entries in the "wasPresent" map have changed in "present or not"
    58  	// status, since the only access was to individual entries via "Get()".
    59  	//
    60  	// If this is non-nil, "SortedKeys()" was accessed. This means we should
    61  	// check for whether this directory has changed or not by checking the
    62  	// "allEntries" array for equality with the existing entries list, since the
    63  	// code asked for all entries and may have used the presence or absence of
    64  	// entries in that list.
    65  	//
    66  	// The goal of having these two checks is to be as narrow as possible to
    67  	// avoid unnecessary rebuilds. If only "Get()" is called on a few entries,
    68  	// then we won't invalidate the build if random unrelated entries are added
    69  	// or removed. But if "SortedKeys()" is called, we need to invalidate the
    70  	// build if anything about the set of entries in this directory is changed.
    71  	allEntries []string
    72  
    73  	mutex sync.Mutex
    74  }
    75  
    76  type DirEntries struct {
    77  	data            map[string]*Entry
    78  	accessedEntries *accessedEntries
    79  	dir             string
    80  }
    81  
    82  func MakeEmptyDirEntries(dir string) DirEntries {
    83  	return DirEntries{dir: dir, data: make(map[string]*Entry)}
    84  }
    85  
    86  type DifferentCase struct {
    87  	Dir    string
    88  	Query  string
    89  	Actual string
    90  }
    91  
    92  func (entries DirEntries) Get(query string) (*Entry, *DifferentCase) {
    93  	if entries.data != nil {
    94  		key := strings.ToLower(query)
    95  		entry := entries.data[key]
    96  
    97  		// Track whether this specific entry was present or absent for watch mode
    98  		if accessed := entries.accessedEntries; accessed != nil {
    99  			accessed.mutex.Lock()
   100  			accessed.wasPresent[key] = entry != nil
   101  			accessed.mutex.Unlock()
   102  		}
   103  
   104  		if entry != nil {
   105  			if entry.base != query {
   106  				return entry, &DifferentCase{
   107  					Dir:    entries.dir,
   108  					Query:  query,
   109  					Actual: entry.base,
   110  				}
   111  			}
   112  			return entry, nil
   113  		}
   114  	}
   115  
   116  	return nil, nil
   117  }
   118  
   119  // This function lets you "peek" at the number of entries without watch mode
   120  // considering the number of entries as having been observed. This is used when
   121  // generating debug log messages to log the number of entries without causing
   122  // watch mode to rebuild when the number of entries has been changed.
   123  func (entries DirEntries) PeekEntryCount() int {
   124  	if entries.data != nil {
   125  		return len(entries.data)
   126  	}
   127  	return 0
   128  }
   129  
   130  func (entries DirEntries) SortedKeys() (keys []string) {
   131  	if entries.data != nil {
   132  		keys = make([]string, 0, len(entries.data))
   133  		for _, entry := range entries.data {
   134  			keys = append(keys, entry.base)
   135  		}
   136  		sort.Strings(keys)
   137  
   138  		// Track the exact set of all entries for watch mode
   139  		if entries.accessedEntries != nil {
   140  			entries.accessedEntries.mutex.Lock()
   141  			entries.accessedEntries.allEntries = keys
   142  			entries.accessedEntries.mutex.Unlock()
   143  		}
   144  
   145  		return keys
   146  	}
   147  
   148  	return
   149  }
   150  
   151  type OpenedFile interface {
   152  	Len() int
   153  	Read(start int, end int) ([]byte, error)
   154  	Close() error
   155  }
   156  
   157  type InMemoryOpenedFile struct {
   158  	Contents []byte
   159  }
   160  
   161  func (f *InMemoryOpenedFile) Len() int {
   162  	return len(f.Contents)
   163  }
   164  
   165  func (f *InMemoryOpenedFile) Read(start int, end int) ([]byte, error) {
   166  	return []byte(f.Contents[start:end]), nil
   167  }
   168  
   169  func (f *InMemoryOpenedFile) Close() error {
   170  	return nil
   171  }
   172  
   173  type FS interface {
   174  	// The returned map is immutable and is cached across invocations. Do not
   175  	// mutate it.
   176  	ReadDirectory(path string) (entries DirEntries, canonicalError error, originalError error)
   177  	ReadFile(path string) (contents string, canonicalError error, originalError error)
   178  	OpenFile(path string) (result OpenedFile, canonicalError error, originalError error)
   179  
   180  	// This is a key made from the information returned by "stat". It is intended
   181  	// to be different if the file has been edited, and to otherwise be equal if
   182  	// the file has not been edited. It should usually work, but no guarantees.
   183  	//
   184  	// See https://apenwarr.ca/log/20181113 for more information about why this
   185  	// can be broken. For example, writing to a file with mmap on WSL on Windows
   186  	// won't change this key. Hopefully this isn't too much of an issue.
   187  	//
   188  	// Additional reading:
   189  	// - https://github.com/npm/npm/pull/20027
   190  	// - https://github.com/golang/go/commit/7dea509703eb5ad66a35628b12a678110fbb1f72
   191  	ModKey(path string) (ModKey, error)
   192  
   193  	// This is part of the interface because the mock interface used for tests
   194  	// should not depend on file system behavior (i.e. different slashes for
   195  	// Windows) while the real interface should.
   196  	IsAbs(path string) bool
   197  	Abs(path string) (string, bool)
   198  	Dir(path string) string
   199  	Base(path string) string
   200  	Ext(path string) string
   201  	Join(parts ...string) string
   202  	Cwd() string
   203  	Rel(base string, target string) (string, bool)
   204  	EvalSymlinks(path string) (string, bool)
   205  
   206  	// This is used in the implementation of "Entry"
   207  	kind(dir string, base string) (symlink string, kind EntryKind)
   208  
   209  	// This is a set of all files used and all directories checked. The build
   210  	// must be invalidated if any of these watched files change.
   211  	WatchData() WatchData
   212  }
   213  
   214  type WatchData struct {
   215  	// These functions return a non-empty path as a string if the file system
   216  	// entry has been modified. For files, the returned path is the same as the
   217  	// file path. For directories, the returned path is either the directory
   218  	// itself or a file in the directory that was changed.
   219  	Paths map[string]func() string
   220  }
   221  
   222  type ModKey struct {
   223  	// What gets filled in here is OS-dependent
   224  	inode      uint64
   225  	size       int64
   226  	mtime_sec  int64
   227  	mtime_nsec int64
   228  	mode       uint32
   229  	uid        uint32
   230  }
   231  
   232  // Some file systems have a time resolution of only a few seconds. If a mtime
   233  // value is too new, we won't be able to tell if it has been recently modified
   234  // or not. So we only use mtimes for comparison if they are sufficiently old.
   235  // Apparently the FAT file system has a resolution of two seconds according to
   236  // this article: https://en.wikipedia.org/wiki/Stat_(system_call).
   237  const modKeySafetyGap = 3 // In seconds
   238  var modKeyUnusable = errors.New("The modification key is unusable")
   239  
   240  // Limit the number of files open simultaneously to avoid ulimit issues
   241  var fileOpenLimit = make(chan bool, 32)
   242  
   243  func BeforeFileOpen() {
   244  	// This will block if the number of open files is already at the limit
   245  	fileOpenLimit <- false
   246  }
   247  
   248  func AfterFileClose() {
   249  	<-fileOpenLimit
   250  }
   251  
   252  // This is a fork of "os.MkdirAll" to work around bugs with the WebAssembly
   253  // build target. More information here: https://github.com/golang/go/issues/43768.
   254  func MkdirAll(fs FS, path string, perm os.FileMode) error {
   255  	// Run "Join" once to run "Clean" on the path, which removes trailing slashes
   256  	return mkdirAll(fs, fs.Join(path), perm)
   257  }
   258  
   259  func mkdirAll(fs FS, path string, perm os.FileMode) error {
   260  	// Fast path: if we can tell whether path is a directory or file, stop with success or error.
   261  	if dir, err := os.Stat(path); err == nil {
   262  		if dir.IsDir() {
   263  			return nil
   264  		}
   265  		return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
   266  	}
   267  
   268  	// Slow path: make sure parent exists and then call Mkdir for path.
   269  	if parent := fs.Dir(path); parent != path {
   270  		// Create parent.
   271  		if err := mkdirAll(fs, parent, perm); err != nil {
   272  			return err
   273  		}
   274  	}
   275  
   276  	// Parent now exists; invoke Mkdir and use its result.
   277  	if err := os.Mkdir(path, perm); err != nil {
   278  		// Handle arguments like "foo/." by
   279  		// double-checking that directory doesn't exist.
   280  		dir, err1 := os.Lstat(path)
   281  		if err1 == nil && dir.IsDir() {
   282  			return nil
   283  		}
   284  		return err
   285  	}
   286  	return nil
   287  }