- Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
On Windows, if node_modules is a Mounted Volume pointing to a volume without drive letter, esbuild won't discover any of the modules inside that folder, and non-relative imports like
import `@storybook/global` will fail, as if the modules weren't there.
X [ERROR] Could not resolve "@storybook/global" ../../node_modules/storybook/dist/core-server/presets/common-manager.js:73:23: 73 │ import { global } from "@storybook/global"; ╵ ~~~~~~~~~~~~~~~~~~~ You can mark the path "@storybook/global" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle. Minimal reproduction here: https://github.com/chenxinyanc/esbuild-resolution-mounted-volume
I accidentally bumped into this issue when trying to set up storybook. It turned out the issue is similar to unrs/unrs-resolver#65 but somehow only affects imports from node_modules.
For context, I've mounted a partition from another SSD to the node_modules folder, so I can keep most of the project files on the original HDD, while still saving some time when installing the dependencies.
Everything worked well (e.g., nodejs itself, or webpack, vite, etc.) until I tried to run storybook today, which directly invokes esbuild under the hood.
Note that for now, NodeJS resolves mounted folder (mounted volume) as if they were ordinary folders. That's why I'm using mounted volume instead of ordinary Junction Points.
(...\esbuild-resolution-mounted-volume\src\node_modules is a mounted volume in the example below.)
...\esbuild-resolution-mounted-volume\src> node Welcome to Node.js v24.8.0. Type ".help" for more information. > const fs = require("fs"); undefined > fs.realpathSync("./node_modules/foo.js"); '...\\esbuild-resolution-mounted-volume\\src\\node_modules\\foo.js' > require("foo") {}It was then I discovered this issue.
I've been using LLM to help with the investigation. I'm not sure whether it might help, but here are the findings from it:
LLM output (Root Cause: `filepath.EvalSymlinks()` fails with "too many links")
Root Cause:
filepath.EvalSymlinks()fails with "too many links"The exact failure chain in esbuild's
realFS.kind():
Step Go API Result 1. os.Lstat("node_modules")Returns Mode: L---------ModeSymlink=true,ModeDir=false2. Check mode & os.ModeSymlinktrue→ enters symlink branch3. filepath.EvalSymlinks("node_modules")"EvalSymlinks: too many links"FAILS 4. return(silently)kind = 0(neither Dir nor File)Entry skipped Because
kind()returns zero,entry.Kind(r.fs) == fs.DirEntryisfalse, sohasNodeModulesstaysfalse, and esbuild never enters the node_modules directory.Why
filepath.EvalSymlinksfailsYour junction points to a volume GUID device path:
\\?\Volume{09a29bad-...}\. Go'sfilepath.EvalSymlinks()repeatedly tries to resolve components, and this device-path form confuses its internal loop — it interprets the volume GUID junction target as another symlink to follow, eventually hitting the maximum link-follow count and returning "too many links".This matches your intuition exactly — junctions pointing to
\\?\device paths should ideally be left unresolved rather than followed throughEvalSymlinks. Node.js handles this by treating junctions as transparent (stat follows them, realpath returns the original path), but Go'sfilepath.EvalSymlinkschokes on the volume GUID target.The specific esbuild code path (from the source you found)
// internal/fs/fs_real.go — realFS.kind() if (mode & os.ModeSymlink) != 0 { link, err := fs.fp.evalSymlinks(entryPath) // ← FAILS HERE: "too many links" if err != nil { return // ← Silent skip! kind=0, entry treated as neither file nor dir } // ...never reached... }
I've also checked other versions of ESBuild and it seems the issue was there from the very beginning.