github.com/evanw/esbuild@v0.21.4/internal/fs/fs_real.go (about) 1 package fs 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "os" 8 "sort" 9 "strings" 10 "sync" 11 "syscall" 12 ) 13 14 type realFS struct { 15 // Stores the file entries for directories we've listed before 16 entries map[string]entriesOrErr 17 18 // This stores data that will end up being returned by "WatchData()" 19 watchData map[string]privateWatchData 20 21 // When building with WebAssembly, the Go compiler doesn't correctly handle 22 // platform-specific path behavior. Hack around these bugs by compiling 23 // support for both Unix and Windows paths into all executables and switch 24 // between them at run-time instead. 25 fp goFilepath 26 27 entriesMutex sync.Mutex 28 watchMutex sync.Mutex 29 30 // If true, do not use the "entries" cache 31 doNotCacheEntries bool 32 } 33 34 type entriesOrErr struct { 35 canonicalError error 36 originalError error 37 entries DirEntries 38 } 39 40 type watchState uint8 41 42 const ( 43 stateNone watchState = iota 44 stateDirHasAccessedEntries // Compare "accessedEntries" 45 stateDirUnreadable // Compare directory readability 46 stateFileHasModKey // Compare "modKey" 47 stateFileNeedModKey // Need to transition to "stateFileHasModKey" or "stateFileUnusableModKey" before "WatchData()" returns 48 stateFileMissing // Compare file presence 49 stateFileUnusableModKey // Compare "fileContents" 50 ) 51 52 type privateWatchData struct { 53 accessedEntries *accessedEntries 54 fileContents string 55 modKey ModKey 56 state watchState 57 } 58 59 type RealFSOptions struct { 60 AbsWorkingDir string 61 WantWatchData bool 62 DoNotCache bool 63 } 64 65 func RealFS(options RealFSOptions) (FS, error) { 66 var fp goFilepath 67 if CheckIfWindows() { 68 fp.isWindows = true 69 fp.pathSeparator = '\\' 70 } else { 71 fp.isWindows = false 72 fp.pathSeparator = '/' 73 } 74 75 // Come up with a default working directory if one was not specified 76 fp.cwd = options.AbsWorkingDir 77 if fp.cwd == "" { 78 if cwd, err := os.Getwd(); err == nil { 79 fp.cwd = cwd 80 } else if fp.isWindows { 81 fp.cwd = "C:\\" 82 } else { 83 fp.cwd = "/" 84 } 85 } else if !fp.isAbs(fp.cwd) { 86 return nil, fmt.Errorf("The working directory %q is not an absolute path", fp.cwd) 87 } 88 89 // Resolve symlinks in the current working directory. Symlinks are resolved 90 // when input file paths are converted to absolute paths because we need to 91 // recognize an input file as unique even if it has multiple symlinks 92 // pointing to it. The build will generate relative paths from the current 93 // working directory to the absolute input file paths for error messages, 94 // so the current working directory should be processed the same way. Not 95 // doing this causes test failures with esbuild when run from inside a 96 // symlinked directory. 97 // 98 // This deliberately ignores errors due to e.g. infinite loops. If there is 99 // an error, we will just use the original working directory and likely 100 // encounter an error later anyway. And if we don't encounter an error 101 // later, then the current working directory didn't even matter and the 102 // error is unimportant. 103 if path, err := fp.evalSymlinks(fp.cwd); err == nil { 104 fp.cwd = path 105 } 106 107 // Only allocate memory for watch data if necessary 108 var watchData map[string]privateWatchData 109 if options.WantWatchData { 110 watchData = make(map[string]privateWatchData) 111 } 112 113 var result FS = &realFS{ 114 entries: make(map[string]entriesOrErr), 115 fp: fp, 116 watchData: watchData, 117 doNotCacheEntries: options.DoNotCache, 118 } 119 120 // Add a wrapper that lets us traverse into ".zip" files. This is what yarn 121 // uses as a package format when in yarn is in its "PnP" mode. 122 result = &zipFS{ 123 inner: result, 124 zipFiles: make(map[string]*zipFile), 125 } 126 127 return result, nil 128 } 129 130 func (fs *realFS) ReadDirectory(dir string) (entries DirEntries, canonicalError error, originalError error) { 131 if !fs.doNotCacheEntries { 132 // First, check the cache 133 cached, ok := func() (cached entriesOrErr, ok bool) { 134 fs.entriesMutex.Lock() 135 defer fs.entriesMutex.Unlock() 136 cached, ok = fs.entries[dir] 137 return 138 }() 139 if ok { 140 // Cache hit: stop now 141 return cached.entries, cached.canonicalError, cached.originalError 142 } 143 } 144 145 // Cache miss: read the directory entries 146 names, canonicalError, originalError := fs.readdir(dir) 147 entries = DirEntries{dir: dir, data: make(map[string]*Entry)} 148 149 // Unwrap to get the underlying error 150 if pathErr, ok := canonicalError.(*os.PathError); ok { 151 canonicalError = pathErr.Unwrap() 152 } 153 154 if canonicalError == nil { 155 for _, name := range names { 156 // Call "stat" lazily for performance. The "@material-ui/icons" package 157 // contains a directory with over 11,000 entries in it and running "stat" 158 // for each entry was a big performance issue for that package. 159 entries.data[strings.ToLower(name)] = &Entry{ 160 dir: dir, 161 base: name, 162 needStat: true, 163 } 164 } 165 } 166 167 // Store data for watch mode 168 if fs.watchData != nil { 169 defer fs.watchMutex.Unlock() 170 fs.watchMutex.Lock() 171 state := stateDirHasAccessedEntries 172 if canonicalError != nil { 173 state = stateDirUnreadable 174 } 175 entries.accessedEntries = &accessedEntries{wasPresent: make(map[string]bool)} 176 fs.watchData[dir] = privateWatchData{ 177 accessedEntries: entries.accessedEntries, 178 state: state, 179 } 180 } 181 182 // Update the cache unconditionally. Even if the read failed, we don't want to 183 // retry again later. The directory is inaccessible so trying again is wasted. 184 if canonicalError != nil { 185 entries.data = nil 186 } 187 if !fs.doNotCacheEntries { 188 fs.entriesMutex.Lock() 189 defer fs.entriesMutex.Unlock() 190 fs.entries[dir] = entriesOrErr{ 191 entries: entries, 192 canonicalError: canonicalError, 193 originalError: originalError, 194 } 195 } 196 return entries, canonicalError, originalError 197 } 198 199 func (fs *realFS) ReadFile(path string) (contents string, canonicalError error, originalError error) { 200 BeforeFileOpen() 201 defer AfterFileClose() 202 buffer, originalError := ioutil.ReadFile(path) 203 canonicalError = fs.canonicalizeError(originalError) 204 205 // Allocate the string once 206 fileContents := string(buffer) 207 208 // Store data for watch mode 209 if fs.watchData != nil { 210 defer fs.watchMutex.Unlock() 211 fs.watchMutex.Lock() 212 data, ok := fs.watchData[path] 213 if canonicalError != nil { 214 data.state = stateFileMissing 215 } else if !ok || data.state == stateDirUnreadable { 216 // Note: If "ReadDirectory" is called before "ReadFile" with this same 217 // path, then "data.state" will be "stateDirUnreadable". In that case 218 // we want to transition to "stateFileNeedModKey" because it's a file. 219 data.state = stateFileNeedModKey 220 } 221 data.fileContents = fileContents 222 fs.watchData[path] = data 223 } 224 225 return fileContents, canonicalError, originalError 226 } 227 228 type realOpenedFile struct { 229 handle *os.File 230 len int 231 } 232 233 func (f *realOpenedFile) Len() int { 234 return f.len 235 } 236 237 func (f *realOpenedFile) Read(start int, end int) ([]byte, error) { 238 bytes := make([]byte, end-start) 239 remaining := bytes 240 241 _, err := f.handle.Seek(int64(start), io.SeekStart) 242 if err != nil { 243 return nil, err 244 } 245 246 for len(remaining) > 0 { 247 n, err := f.handle.Read(remaining) 248 if err != nil && n <= 0 { 249 return nil, err 250 } 251 remaining = remaining[n:] 252 } 253 254 return bytes, nil 255 } 256 257 func (f *realOpenedFile) Close() error { 258 return f.handle.Close() 259 } 260 261 func (fs *realFS) OpenFile(path string) (OpenedFile, error, error) { 262 BeforeFileOpen() 263 defer AfterFileClose() 264 265 f, err := os.Open(path) 266 if err != nil { 267 return nil, fs.canonicalizeError(err), err 268 } 269 270 info, err := f.Stat() 271 if err != nil { 272 f.Close() 273 return nil, fs.canonicalizeError(err), err 274 } 275 276 return &realOpenedFile{f, int(info.Size())}, nil, nil 277 } 278 279 func (fs *realFS) ModKey(path string) (ModKey, error) { 280 BeforeFileOpen() 281 defer AfterFileClose() 282 key, err := modKey(path) 283 284 // Store data for watch mode 285 if fs.watchData != nil { 286 defer fs.watchMutex.Unlock() 287 fs.watchMutex.Lock() 288 data, ok := fs.watchData[path] 289 if !ok { 290 if err == modKeyUnusable { 291 data.state = stateFileUnusableModKey 292 } else if err != nil { 293 data.state = stateFileMissing 294 } else { 295 data.state = stateFileHasModKey 296 } 297 } else if data.state == stateFileNeedModKey { 298 data.state = stateFileHasModKey 299 } 300 data.modKey = key 301 fs.watchData[path] = data 302 } 303 304 return key, err 305 } 306 307 func (fs *realFS) IsAbs(p string) bool { 308 return fs.fp.isAbs(p) 309 } 310 311 func (fs *realFS) Abs(p string) (string, bool) { 312 abs, err := fs.fp.abs(p) 313 return abs, err == nil 314 } 315 316 func (fs *realFS) Dir(p string) string { 317 return fs.fp.dir(p) 318 } 319 320 func (fs *realFS) Base(p string) string { 321 return fs.fp.base(p) 322 } 323 324 func (fs *realFS) Ext(p string) string { 325 return fs.fp.ext(p) 326 } 327 328 func (fs *realFS) Join(parts ...string) string { 329 return fs.fp.clean(fs.fp.join(parts)) 330 } 331 332 func (fs *realFS) Cwd() string { 333 return fs.fp.cwd 334 } 335 336 func (fs *realFS) Rel(base string, target string) (string, bool) { 337 if rel, err := fs.fp.rel(base, target); err == nil { 338 return rel, true 339 } 340 return "", false 341 } 342 343 func (fs *realFS) EvalSymlinks(path string) (string, bool) { 344 if path, err := fs.fp.evalSymlinks(path); err == nil { 345 return path, true 346 } 347 return "", false 348 } 349 350 func (fs *realFS) readdir(dirname string) (entries []string, canonicalError error, originalError error) { 351 BeforeFileOpen() 352 defer AfterFileClose() 353 f, originalError := os.Open(dirname) 354 canonicalError = fs.canonicalizeError(originalError) 355 356 // Stop now if there was an error 357 if canonicalError != nil { 358 return nil, canonicalError, originalError 359 } 360 361 defer f.Close() 362 entries, originalError = f.Readdirnames(-1) 363 canonicalError = originalError 364 365 // Unwrap to get the underlying error 366 if syscallErr, ok := canonicalError.(*os.SyscallError); ok { 367 canonicalError = syscallErr.Unwrap() 368 } 369 370 // Don't convert ENOTDIR to ENOENT here. ENOTDIR is a legitimate error 371 // condition for Readdirnames() on non-Windows platforms. 372 373 // Go's WebAssembly implementation returns EINVAL instead of ENOTDIR if we 374 // call "readdir" on a file. Canonicalize this to ENOTDIR so esbuild's path 375 // resolution code continues traversing instead of failing with an error. 376 // https://github.com/golang/go/blob/2449bbb5e614954ce9e99c8a481ea2ee73d72d61/src/syscall/fs_js.go#L144 377 if pathErr, ok := canonicalError.(*os.PathError); ok && pathErr.Unwrap() == syscall.EINVAL { 378 canonicalError = syscall.ENOTDIR 379 } 380 381 return entries, canonicalError, originalError 382 } 383 384 func (fs *realFS) canonicalizeError(err error) error { 385 // Unwrap to get the underlying error 386 if pathErr, ok := err.(*os.PathError); ok { 387 err = pathErr.Unwrap() 388 } 389 390 // Windows is much more restrictive than Unix about file names. If a file name 391 // is invalid, it will return ERROR_INVALID_NAME. Treat this as ENOENT (i.e. 392 // "the file does not exist") so that the resolver continues trying to resolve 393 // the path on this failure instead of aborting with an error. 394 if fs.fp.isWindows && is_ERROR_INVALID_NAME(err) { 395 err = syscall.ENOENT 396 } 397 398 // Windows returns ENOTDIR here even though nothing we've done yet has asked 399 // for a directory. This really means ENOENT on Windows. Return ENOENT here 400 // so callers that check for ENOENT will successfully detect this file as 401 // missing. 402 if err == syscall.ENOTDIR { 403 err = syscall.ENOENT 404 } 405 406 return err 407 } 408 409 func (fs *realFS) kind(dir string, base string) (symlink string, kind EntryKind) { 410 entryPath := fs.fp.join([]string{dir, base}) 411 412 // Use "lstat" since we want information about symbolic links 413 BeforeFileOpen() 414 defer AfterFileClose() 415 stat, err := os.Lstat(entryPath) 416 if err != nil { 417 return 418 } 419 mode := stat.Mode() 420 421 // Follow symlinks now so the cache contains the translation 422 if (mode & os.ModeSymlink) != 0 { 423 link, err := fs.fp.evalSymlinks(entryPath) 424 if err != nil { 425 return // Skip over this entry 426 } 427 428 // Re-run "lstat" on the symlink target to see if it's a file or not 429 stat2, err2 := os.Lstat(link) 430 if err2 != nil { 431 return // Skip over this entry 432 } 433 mode = stat2.Mode() 434 if (mode & os.ModeSymlink) != 0 { 435 return // This should no longer be a symlink, so this is unexpected 436 } 437 symlink = link 438 } 439 440 // We consider the entry either a directory or a file 441 if (mode & os.ModeDir) != 0 { 442 kind = DirEntry 443 } else { 444 kind = FileEntry 445 } 446 return 447 } 448 449 func (fs *realFS) WatchData() WatchData { 450 paths := make(map[string]func() string) 451 452 for path, data := range fs.watchData { 453 // Each closure below needs its own copy of these loop variables 454 path := path 455 data := data 456 457 // Each function should return true if the state has been changed 458 if data.state == stateFileNeedModKey { 459 key, err := modKey(path) 460 if err == modKeyUnusable { 461 data.state = stateFileUnusableModKey 462 } else if err != nil { 463 data.state = stateFileMissing 464 } else { 465 data.state = stateFileHasModKey 466 data.modKey = key 467 } 468 } 469 470 switch data.state { 471 case stateDirUnreadable: 472 paths[path] = func() string { 473 _, err, _ := fs.readdir(path) 474 if err == nil { 475 return path 476 } 477 return "" 478 } 479 480 case stateDirHasAccessedEntries: 481 paths[path] = func() string { 482 names, err, _ := fs.readdir(path) 483 if err != nil { 484 return path 485 } 486 data.accessedEntries.mutex.Lock() 487 defer data.accessedEntries.mutex.Unlock() 488 if allEntries := data.accessedEntries.allEntries; allEntries != nil { 489 // Check all entries 490 if len(names) != len(allEntries) { 491 return path 492 } 493 sort.Strings(names) 494 for i, s := range names { 495 if s != allEntries[i] { 496 return path 497 } 498 } 499 } else { 500 // Check individual entries 501 lookup := make(map[string]string, len(names)) 502 for _, name := range names { 503 lookup[strings.ToLower(name)] = name 504 } 505 for name, wasPresent := range data.accessedEntries.wasPresent { 506 if originalName, isPresent := lookup[name]; wasPresent != isPresent { 507 return fs.Join(path, originalName) 508 } 509 } 510 } 511 return "" 512 } 513 514 case stateFileMissing: 515 paths[path] = func() string { 516 if info, err := os.Stat(path); err == nil && !info.IsDir() { 517 return path 518 } 519 return "" 520 } 521 522 case stateFileHasModKey: 523 paths[path] = func() string { 524 if key, err := modKey(path); err != nil || key != data.modKey { 525 return path 526 } 527 return "" 528 } 529 530 case stateFileUnusableModKey: 531 paths[path] = func() string { 532 if buffer, err := ioutil.ReadFile(path); err != nil || string(buffer) != data.fileContents { 533 return path 534 } 535 return "" 536 } 537 } 538 } 539 540 return WatchData{ 541 Paths: paths, 542 } 543 }