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 }