Skip to content

Commit

Permalink
warn about issues with case-sensitive paths
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Feb 17, 2021
1 parent 5b346ca commit 5776b21
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 93 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@

Now paths starting with a `/` are always treated as an absolute path on all platforms. This means you can no longer import files at a relative path that starts with `/` on Windows. You should be using a `./` prefix instead.

* Warn when importing a path with the wrong case

Importing a path with the wrong case (e.g. `File.js` instead of `file.js`) will work on Windows and sometimes on macOS because they have case-insensitive file systems, but it will never work on Linux because it has a case-sensitive file system. To help you make your code more portable and to avoid cross-platform build failures, esbuild now issues a warning when you do this.

## 0.8.46

* Fix minification of `.0` in CSS ([#804](https://github.com/evanw/esbuild/issues/804))
Expand Down
16 changes: 14 additions & 2 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,19 @@ func runOnResolvePlugins(
// Resolve relative to the resolve directory by default. All paths in the
// "file" namespace automatically have a resolve directory. Loader plugins
// can also configure a custom resolve directory for files in other namespaces.
return res.Resolve(absResolveDir, path, kind), false
result := res.Resolve(absResolveDir, path, kind)

// Warn when the case used for importing differs from the actual file name
if result != nil && result.DifferentCase != nil {
diffCase := *result.DifferentCase
log.AddRangeWarning(importSource, importPathRange, fmt.Sprintf(
"Use %q instead of %q to avoid issues with case-sensitive file systems",
res.PrettyPath(logger.Path{Text: fs.Join(diffCase.Dir, diffCase.Actual), Namespace: "file"}),
res.PrettyPath(logger.Path{Text: fs.Join(diffCase.Dir, diffCase.Query), Namespace: "file"}),
))
}

return result, false
}

type loaderPluginResult struct {
Expand Down Expand Up @@ -1114,7 +1126,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(s.fs) == fs.FileEntry {
if entry, _ := entries.Get(base); entry != nil && entry.Kind(s.fs) == fs.FileEntry {
entryPoints[i] = "./" + path
}
}
Expand Down
40 changes: 39 additions & 1 deletion internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fs

import (
"errors"
"strings"
"sync"
)

Expand Down Expand Up @@ -41,10 +42,47 @@ func (e *Entry) Symlink(fs FS) string {
return e.symlink
}

type DirEntries struct {
dir string
data map[string]*Entry
}

type DifferentCase struct {
Dir string
Query string
Actual string
}

func (entries DirEntries) Get(query string) (*Entry, *DifferentCase) {
if entries.data != nil {
if entry := entries.data[strings.ToLower(query)]; entry != nil {
if entry.base != query {
return entry, &DifferentCase{
Dir: entries.dir,
Query: query,
Actual: entry.base,
}
}
return entry, nil
}
}
return nil, nil
}

func (entries DirEntries) UnorderedKeys() (keys []string) {
if entries.data != nil {
keys = make([]string, 0, len(entries.data))
for _, entry := range entries.data {
keys = append(keys, entry.base)
}
}
return
}

type FS interface {
// The returned map is immutable and is cached across invocations. Do not
// mutate it.
ReadDirectory(path string) (map[string]*Entry, error)
ReadDirectory(path string) (DirEntries, error)
ReadFile(path string) (string, error)

// This is a key made from the information returned by "stat". It is intended
Expand Down
20 changes: 10 additions & 10 deletions internal/fs/fs_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import (
)

type mockFS struct {
dirs map[string]map[string]*Entry
dirs map[string]DirEntries
files map[string]string
}

func MockFS(input map[string]string) FS {
dirs := make(map[string]map[string]*Entry)
dirs := make(map[string]DirEntries)
files := make(map[string]string)

for k, v := range input {
Expand All @@ -29,16 +29,17 @@ func MockFS(input map[string]string) FS {
kDir := path.Dir(k)
dir, ok := dirs[kDir]
if !ok {
dir = make(map[string]*Entry)
dir = DirEntries{kDir, make(map[string]*Entry)}
dirs[kDir] = dir
}
if kDir == k {
break
}
base := path.Base(k)
if k == original {
dir[path.Base(k)] = &Entry{kind: FileEntry}
dir.data[strings.ToLower(base)] = &Entry{kind: FileEntry, base: base}
} else {
dir[path.Base(k)] = &Entry{kind: DirEntry}
dir.data[strings.ToLower(base)] = &Entry{kind: DirEntry, base: base}
}
k = kDir
}
Expand All @@ -47,12 +48,11 @@ func MockFS(input map[string]string) FS {
return &mockFS{dirs, files}
}

func (fs *mockFS) ReadDirectory(path string) (map[string]*Entry, error) {
dir := fs.dirs[path]
if dir == nil {
return nil, syscall.ENOENT
func (fs *mockFS) ReadDirectory(path string) (DirEntries, error) {
if dir, ok := fs.dirs[path]; ok {
return dir, nil
}
return dir, nil
return DirEntries{}, syscall.ENOENT
}

func (fs *mockFS) ReadFile(path string) (string, error) {
Expand Down
14 changes: 12 additions & 2 deletions internal/fs/fs_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ func TestMockFSBasic(t *testing.T) {
if err != nil {
t.Fatal("Expected to find /src")
}
if len(src) != 2 || src["index.js"].Kind(fs) != FileEntry || src["util.js"].Kind(fs) != FileEntry {
indexEntry, _ := src.Get("index.js")
utilEntry, _ := src.Get("util.js")
if len(src.data) != 2 ||
indexEntry == nil || indexEntry.Kind(fs) != FileEntry ||
utilEntry == nil || utilEntry.Kind(fs) != FileEntry {
t.Fatalf("Incorrect contents for /src: %v", src)
}

Expand All @@ -57,7 +61,13 @@ func TestMockFSBasic(t *testing.T) {
if err != nil {
t.Fatal("Expected to find /")
}
if len(slash) != 3 || slash["src"].Kind(fs) != DirEntry || slash["README.md"].Kind(fs) != FileEntry || slash["package.json"].Kind(fs) != FileEntry {
srcEntry, _ := slash.Get("src")
readmeEntry, _ := slash.Get("README.md")
packageEntry, _ := slash.Get("package.json")
if len(slash.data) != 3 ||
srcEntry == nil || srcEntry.Kind(fs) != DirEntry ||
readmeEntry == nil || readmeEntry.Kind(fs) != FileEntry ||
packageEntry == nil || packageEntry.Kind(fs) != FileEntry {
t.Fatalf("Incorrect contents for /: %v", slash)
}
}
Expand Down
11 changes: 6 additions & 5 deletions internal/fs/fs_real.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"sort"
"strings"
"sync"
"syscall"
)
Expand All @@ -28,7 +29,7 @@ type realFS struct {
}

type entriesOrErr struct {
entries map[string]*Entry
entries DirEntries
err error
}

Expand Down Expand Up @@ -113,7 +114,7 @@ func RealFS(options RealFSOptions) (FS, error) {
}, nil
}

func (fs *realFS) ReadDirectory(dir string) (map[string]*Entry, error) {
func (fs *realFS) ReadDirectory(dir string) (DirEntries, error) {
if !fs.doNotCacheEntries {
// First, check the cache
if cached, ok := fs.entries[dir]; ok {
Expand All @@ -124,13 +125,13 @@ func (fs *realFS) ReadDirectory(dir string) (map[string]*Entry, error) {

// Cache miss: read the directory entries
names, err := readdir(dir)
entries := make(map[string]*Entry)
entries := DirEntries{dir, make(map[string]*Entry)}
if err == nil {
for _, name := range names {
// Call "stat" lazily for performance. The "@material-ui/icons" package
// contains a directory with over 11,000 entries in it and running "stat"
// for each entry was a big performance issue for that package.
entries[name] = &Entry{
entries.data[strings.ToLower(name)] = &Entry{
dir: dir,
base: name,
needStat: true,
Expand All @@ -156,7 +157,7 @@ func (fs *realFS) ReadDirectory(dir string) (map[string]*Entry, error) {
// Update the cache unconditionally. Even if the read failed, we don't want to
// retry again later. The directory is inaccessible so trying again is wasted.
if err != nil {
entries = nil
entries.data = nil
}
if !fs.doNotCacheEntries {
fs.entries[dir] = entriesOrErr{entries: entries, err: err}
Expand Down
Loading

0 comments on commit 5776b21

Please sign in to comment.