rspack_resolver/
file_system.rs

1use std::{
2  fs, io,
3  path::{Path, PathBuf},
4};
5
6use cfg_if::cfg_if;
7#[cfg(feature = "yarn_pnp")]
8use pnp::fs::{LruZipCache, VPath, VPathInfo, ZipCache};
9
10/// File System abstraction used for `ResolverGeneric`
11#[async_trait::async_trait]
12pub trait FileSystem {
13  /// See [std::fs::read]
14  ///
15  /// # Errors
16  ///
17  /// See [std::fs::read]
18  async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
19  /// See [std::fs::read_to_string]
20  ///
21  /// # Errors
22  ///
23  /// * See [std::fs::read_to_string]
24  /// ## Warning
25  /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
26  /// because object safety requirements, it is especially useful, when
27  /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
28  /// napi env.
29  async fn read_to_string(&self, path: &Path) -> io::Result<String>;
30
31  /// See [std::fs::metadata]
32  ///
33  /// # Errors
34  /// See [std::fs::metadata]
35  /// ## Warning
36  /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
37  /// because object safety requirements, it is especially useful, when
38  /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
39  /// napi env.
40  async fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
41
42  /// See [std::fs::symlink_metadata]
43  ///
44  /// # Errors
45  ///
46  /// See [std::fs::symlink_metadata]
47  /// ## Warning
48  /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
49  /// because object safety requirements, it is especially useful, when
50  /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
51  /// napi env.
52  async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
53
54  /// See [std::fs::canonicalize]
55  ///
56  /// # Errors
57  ///
58  /// See [std::fs::read_link]
59  /// ## Warning
60  /// Use `&Path` instead of a generic `P: AsRef<Path>` here,
61  /// because object safety requirements, it is especially useful, when
62  /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric<Fs>` in
63  /// napi env.
64  async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
65}
66
67/// Metadata information about a file
68#[derive(Debug, Clone, Copy)]
69pub struct FileMetadata {
70  pub is_file: bool,
71  pub is_dir: bool,
72  pub is_symlink: bool,
73}
74
75impl FileMetadata {
76  pub fn new(is_file: bool, is_dir: bool, is_symlink: bool) -> Self {
77    Self {
78      is_file,
79      is_dir,
80      is_symlink,
81    }
82  }
83}
84
85#[cfg(feature = "yarn_pnp")]
86impl From<pnp::fs::FileType> for FileMetadata {
87  fn from(value: pnp::fs::FileType) -> Self {
88    Self::new(
89      value == pnp::fs::FileType::File,
90      value == pnp::fs::FileType::Directory,
91      false,
92    )
93  }
94}
95
96impl From<fs::Metadata> for FileMetadata {
97  fn from(metadata: fs::Metadata) -> Self {
98    Self::new(metadata.is_file(), metadata.is_dir(), metadata.is_symlink())
99  }
100}
101
102pub struct FileSystemOptions {
103  #[cfg(feature = "yarn_pnp")]
104  pub enable_pnp: bool,
105}
106
107impl Default for FileSystemOptions {
108  fn default() -> Self {
109    Self {
110      #[cfg(feature = "yarn_pnp")]
111      enable_pnp: true,
112    }
113  }
114}
115
116/// Operating System
117pub struct FileSystemOs {
118  options: FileSystemOptions,
119  #[cfg(feature = "yarn_pnp")]
120  pnp_lru: LruZipCache<Vec<u8>>,
121}
122
123impl Default for FileSystemOs {
124  fn default() -> Self {
125    Self {
126      options: FileSystemOptions::default(),
127      #[cfg(feature = "yarn_pnp")]
128      pnp_lru: LruZipCache::new(50, pnp::fs::open_zip_via_read_p),
129    }
130  }
131}
132
133#[cfg(not(target_arch = "wasm32"))]
134#[async_trait::async_trait]
135impl FileSystem for FileSystemOs {
136  async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
137    cfg_if! {
138      if #[cfg(feature = "yarn_pnp")] {
139        if self.options.enable_pnp {
140            return match VPath::from(path)? {
141                VPath::Zip(info) => self.pnp_lru.read(info.physical_base_path(), info.zip_path),
142                VPath::Virtual(info) => tokio::fs::read(info.physical_base_path()).await,
143                VPath::Native(path) => tokio::fs::read(&path).await,
144            }
145        }
146    }}
147
148    tokio::fs::read(path).await
149  }
150
151  async fn read_to_string(&self, path: &Path) -> io::Result<String> {
152    cfg_if! {
153    if #[cfg(feature = "yarn_pnp")] {
154        if self.options.enable_pnp {
155            return match VPath::from(path)? {
156                VPath::Zip(info) => self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path),
157                VPath::Virtual(info) => tokio::fs::read_to_string(info.physical_base_path()).await,
158                VPath::Native(path) => tokio::fs::read_to_string(&path).await,
159                }
160            }
161        }
162    }
163    tokio::fs::read_to_string(path).await
164  }
165
166  async fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
167    cfg_if! {
168        if #[cfg(feature = "yarn_pnp")] {
169            if self.options.enable_pnp {
170                return match VPath::from(path)? {
171                    VPath::Zip(info) => self
172                        .pnp_lru
173                        .file_type(info.physical_base_path(), info.zip_path)
174                        .map(FileMetadata::from),
175                    VPath::Virtual(info) => {
176                        tokio::fs::metadata(info.physical_base_path())
177                            .await
178                            .map(FileMetadata::from)
179                    }
180                    VPath::Native(path) => tokio::fs::metadata(path).await.map(FileMetadata::from),
181                }
182            }
183        }
184    }
185
186    tokio::fs::metadata(path).await.map(FileMetadata::from)
187  }
188
189  async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
190    tokio::fs::symlink_metadata(path)
191      .await
192      .map(FileMetadata::from)
193  }
194
195  async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
196    cfg_if! {
197        if #[cfg(feature = "yarn_pnp")] {
198            if self.options.enable_pnp {
199                return match VPath::from(path)? {
200                    VPath::Zip(info) => {
201                        dunce::canonicalize(info.physical_base_path().join(info.zip_path))
202                    }
203                    VPath::Virtual(info) => dunce::canonicalize(info.physical_base_path()),
204                    VPath::Native(path) => dunce::canonicalize(path),
205                }
206            }
207        }
208    }
209
210    dunce::canonicalize(path)
211  }
212}
213
214#[cfg(target_arch = "wasm32")]
215#[async_trait::async_trait]
216impl FileSystem for FileSystemOs {
217  async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
218    std::fs::read(path)
219  }
220
221  async fn read_to_string(&self, path: &Path) -> io::Result<String> {
222    std::fs::read_to_string(path)
223  }
224
225  async fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
226    // This implementation is verbose because there might be something wrong node wasm runtime.
227    // I will investigate it in the future.
228    if let Ok(m) = std::fs::metadata(path).map(FileMetadata::from) {
229      return Ok(m);
230    }
231
232    self.symlink_metadata(path).await?;
233    let path = self.canonicalize(path).await?;
234    std::fs::metadata(path).map(FileMetadata::from)
235  }
236
237  async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
238    std::fs::symlink_metadata(path).map(FileMetadata::from)
239  }
240
241  async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
242    use std::path::Component;
243    let mut path_buf = path.to_path_buf();
244    let link = fs::read_link(&path_buf)?;
245    path_buf.pop();
246    for component in link.components() {
247      match component {
248        Component::ParentDir => {
249          path_buf.pop();
250        }
251        Component::Normal(seg) => {
252          path_buf.push(seg.to_string_lossy().trim_end_matches('\0'));
253        }
254        Component::RootDir => {
255          path_buf = PathBuf::from("/");
256        }
257        Component::CurDir | Component::Prefix(_) => {}
258      }
259
260      // This is not performant, we may optimize it with cache in the future
261      if fs::symlink_metadata(&path_buf).is_ok_and(|m| m.is_symlink()) {
262        let dir = self.canonicalize(&path_buf).await?;
263        path_buf = dir;
264      }
265    }
266    Ok(path_buf)
267  }
268}
269
270#[tokio::test]
271async fn metadata() {
272  let meta = FileMetadata {
273    is_file: true,
274    is_dir: true,
275    is_symlink: true,
276  };
277  assert_eq!(
278    format!("{meta:?}"),
279    "FileMetadata { is_file: true, is_dir: true, is_symlink: true }"
280  );
281  let _ = meta;
282}