esbuild: misleading or nonexistent errors in path resolving
I stumbled upon this bug while figuring out how to run the native version of esbuild
on my machine.
If your default ulimit -n
max open file limit is low then esbuild
error logging is misleading and not very helpful in determining the cause of the problem. In some cases it can also lead to incorrect bundling output without any error messages logged.
Scenario 1: Bundling projects with a large number of source files
Attempt to bundle react-admin/examples/simple
with the default max open file limit on my machine:
$ ulimit -n
256
$ esbuild --bundle --minify --loader:.js=jsx --define:process.env.NODE_ENV='"production"' --define:global=window --outfile=dist/main.js src/index.js
node_modules/ra-language-french/tsconfig.json:2:15: warning: Cannot find base config file "../../tsconfig.json"
"extends": "../../tsconfig.json",
~~~~~~~~~~~~~~~~~~~~~
node_modules/ra-ui-materialui/esm/list/DatagridHeaderCell.js:27:22: error: Could not resolve "@material-ui/core/TableCell"
import TableCell from '@material-ui/core/TableCell';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
node_modules/ra-ui-materialui/esm/list/DatagridHeaderCell.js:28:27: error: Could not resolve "@material-ui/core/TableSortLabel"
import TableSortLabel from '@material-ui/core/TableSortLabel';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...
1 warning and 10 errors reached (disable error limit with --error-limit=0)
Strangely, esbuild-wasm
worked correctly with the same ulimit -n 256
.
Scenario 2: Incorrect bundle output for require()
statements in presence of EMFILE
errors (too many open files)
Given the following source file and a dozen require
’d files with console.log()
side effects:
$ cat main.js
try {
// all these required files exist and are well-formed
require("./z1");
require("./z2");
require("./z3");
require("./z4");
require("./z5");
require("./z6");
require("./z7");
require("./z8");
require("./z9");
require("./z10");
require("./z11");
require("./z12");
} catch (x) {}
It works as expected with a sufficiently high ulimit -n
value:
$ esbuild main.js --bundle
(() => {
var __commonJS = (callback, module) => () => {
if (!module) {
module = {exports: {}};
callback(module.exports, module);
}
return module.exports;
};
// z1.js
var require_z1 = __commonJS(() => {
console.log(1);
});
// z2.js
var require_z2 = __commonJS(() => {
console.log(2);
});
// z3.js
var require_z3 = __commonJS(() => {
console.log(3);
});
// z4.js
var require_z4 = __commonJS(() => {
console.log(4);
});
// z5.js
var require_z5 = __commonJS(() => {
console.log(5);
});
// z6.js
var require_z6 = __commonJS(() => {
console.log(6);
});
// z7.js
var require_z7 = __commonJS(() => {
console.log(7);
});
// z8.js
var require_z8 = __commonJS(() => {
console.log(8);
});
// z9.js
var require_z9 = __commonJS(() => {
console.log(9);
});
// z10.js
var require_z10 = __commonJS(() => {
console.log(10);
});
// z11.js
var require_z11 = __commonJS(() => {
console.log(11);
});
// z12.js
var require_z12 = __commonJS(() => {
console.log(12);
});
// main.js
try {
require_z1();
require_z2();
require_z3();
require_z4();
require_z5();
require_z6();
require_z7();
require_z8();
require_z9();
require_z10();
require_z11();
require_z12();
} catch (x) {
}
})();
But if the ulimit -n
value is too low, then the following incorrect output is produced without error or warning - notice the requires are not inlined into the bundle:
$ ulimit -n 11
$ esbuild main.js --bundle
(() => {
// main.js
try {
require("./z1");
require("./z2");
require("./z3");
require("./z4");
require("./z5");
require("./z6");
require("./z7");
require("./z8");
require("./z9");
require("./z10");
require("./z11");
require("./z12");
} catch (x) {
}
})();
$ echo $?
0
It appears that esbuild
’s path resolving algorithm incorrectly treats all errors as if they were ENOENT
- no such file or directory.
The following patch was useful in debugging the cause of the Scenario 1 failure and might be useful as a starting point for a fix:
--- a/internal/fs/fs.go
+++ b/internal/fs/fs.go
@@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"sync"
+ "syscall"
)
type EntryKind uint8
@@ -301,6 +302,9 @@ func (fs *realFS) ReadDirectory(dir string) map[string]*Entry {
func (fs *realFS) ReadFile(path string) (string, bool) {
buffer, err := ioutil.ReadFile(path)
+ if err != nil && err.(*os.PathError).Err.(syscall.Errno) != syscall.ENOENT {
+ panic(err)
+ }
return string(buffer), err == nil
}
@@ -339,6 +343,9 @@ func (*realFS) Rel(base string, target string) (string, bool) {
func readdir(dirname string) ([]string, error) {
f, err := os.Open(dirname)
if err != nil {
+ if err.(*os.PathError).Err.(syscall.Errno) != syscall.ENOENT {
+ panic(err)
+ }
return nil, err
}
defer f.Close()
Unfortunately, this patch did not log any errors for very low ulimit -n
values - such as in Scenario 2.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 19 (18 by maintainers)
Commits related to this issue
- limit simultaneously open file handles (#348) — committed to evanw/esbuild by evanw 4 years ago
- limit simultaneously open file handles (#348) — committed to evanw/esbuild by evanw 4 years ago
- prevent ulimit overflow when writing too (#348) — committed to evanw/esbuild by evanw 4 years ago
- pass file system errors to callers (#348) — committed to evanw/esbuild by evanw 4 years ago
- pass directory errors to callers (#348) — committed to evanw/esbuild by evanw 4 years ago
Yes, I agree. That’s why I left this open. I’ll try to fix that too. I’d rather feed the error through to the log instead of causing a panic though. That will also give more context on the failure.