From a539c40c48254e4c78c793368987862c7c3b0df1 Mon Sep 17 00:00:00 2001 From: Boshen Date: Fri, 29 May 2026 13:27:08 +0000 Subject: [PATCH] fix: make symlink_metadata VPath-aware for Yarn PnP (#1183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `FileSystemOs::metadata` already translates Yarn PnP **virtual** and **zip** paths through `VPath` before hitting the filesystem, but `symlink_metadata` did not — it `lstat`'d the raw path directly: ```rust fn symlink_metadata(&self, path: &Path) -> io::Result { Self::symlink_metadata(path) // no VPath translation } ``` For a virtual/zip path that has no physical existence, that `lstat` fails, so canonicalization (`canonicalize_with_visited`) sees the error, treats the path as a non-symlink, and cannot resolve symlinks correctly under PnP. This makes `symlink_metadata` mirror `metadata`'s `VPath` handling. Zip entries are never symlinks, so they reuse the same `file_type` lookup as `metadata`; virtual/native paths fall through to the real `lstat` on the translated physical path. This is also a prerequisite for #1182, which routes `is_file`/`is_dir` through the cached `lstat` and so requires `symlink_metadata` to be VPath-aware. ## Verification - `cargo test --all-features` (includes the Yarn PnP suite) — green - `fixtures/pnp` JS resolver test — green 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src/file_system.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/file_system.rs b/src/file_system.rs index c23eb57dc..a767b3dc7 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -295,6 +295,26 @@ impl FileSystem for FileSystemOs { } fn symlink_metadata(&self, path: &Path) -> io::Result { + cfg_if! { + if #[cfg(feature = "yarn_pnp")] { + if self.yarn_pnp { + // Mirror `metadata`'s virtual-path translation. Without it, `lstat`-ing a + // virtual or zip path directly stats a path that may not physically exist, so + // canonicalization cannot detect symlinks correctly under Yarn PnP. Zip entries + // are never symlinks, so reuse the same file-type lookup as `metadata`. + return match VPath::from(path)? { + VPath::Zip(info) => self + .pnp_lru + .file_type(info.physical_base_path(), info.zip_path) + .map(FileMetadata::from), + VPath::Virtual(info) => { + Self::symlink_metadata(&info.physical_base_path()) + } + VPath::Native(path) => Self::symlink_metadata(&path), + } + } + } + } Self::symlink_metadata(path) }