Skip to main content

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
133impl FileSystemOs {
134  pub fn new(options: FileSystemOptions) -> Self {
135    Self {
136      options,
137      #[cfg(feature = "yarn_pnp")]
138      pnp_lru: LruZipCache::new(50, pnp::fs::open_zip_via_read_p),
139    }
140  }
141}
142
143#[cfg(not(target_arch = "wasm32"))]
144#[async_trait::async_trait]
145impl FileSystem for FileSystemOs {
146  async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
147    cfg_if! {
148      if #[cfg(feature = "yarn_pnp")] {
149        if self.options.enable_pnp {
150            return match VPath::from(path)? {
151                VPath::Zip(info) => self.pnp_lru.read(info.physical_base_path(), info.zip_path),
152                VPath::Virtual(info) => tokio::fs::read(info.physical_base_path()).await,
153                VPath::Native(path) => tokio::fs::read(&path).await,
154            }
155        }
156    }}
157
158    tokio::fs::read(path).await
159  }
160
161  async fn read_to_string(&self, path: &Path) -> io::Result<String> {
162    cfg_if! {
163    if #[cfg(feature = "yarn_pnp")] {
164        if self.options.enable_pnp {
165            return match VPath::from(path)? {
166                VPath::Zip(info) => self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path),
167                VPath::Virtual(info) => tokio::fs::read_to_string(info.physical_base_path()).await,
168                VPath::Native(path) => tokio::fs::read_to_string(&path).await,
169                }
170            }
171        }
172    }
173    tokio::fs::read_to_string(path).await
174  }
175
176  async fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
177    cfg_if! {
178        if #[cfg(feature = "yarn_pnp")] {
179            if self.options.enable_pnp {
180                return match VPath::from(path)? {
181                    VPath::Zip(info) => self
182                        .pnp_lru
183                        .file_type(info.physical_base_path(), info.zip_path)
184                        .map(FileMetadata::from),
185                    VPath::Virtual(info) => {
186                        tokio::fs::metadata(info.physical_base_path())
187                            .await
188                            .map(FileMetadata::from)
189                    }
190                    VPath::Native(path) => tokio::fs::metadata(path).await.map(FileMetadata::from),
191                }
192            }
193        }
194    }
195
196    tokio::fs::metadata(path).await.map(FileMetadata::from)
197  }
198
199  async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
200    tokio::fs::symlink_metadata(path)
201      .await
202      .map(FileMetadata::from)
203  }
204
205  async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
206    cfg_if! {
207        if #[cfg(feature = "yarn_pnp")] {
208            if self.options.enable_pnp {
209                return match VPath::from(path)? {
210                    VPath::Zip(info) => {
211                        dunce::canonicalize(info.physical_base_path().join(info.zip_path))
212                    }
213                    VPath::Virtual(info) => dunce::canonicalize(info.physical_base_path()),
214                    VPath::Native(path) => dunce::canonicalize(path),
215                }
216            }
217        }
218    }
219
220    dunce::canonicalize(path)
221  }
222}
223
224#[cfg(target_arch = "wasm32")]
225#[async_trait::async_trait]
226impl FileSystem for FileSystemOs {
227  async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
228    std::fs::read(path)
229  }
230
231  async fn read_to_string(&self, path: &Path) -> io::Result<String> {
232    std::fs::read_to_string(path)
233  }
234
235  async fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
236    // This implementation is verbose because there might be something wrong node wasm runtime.
237    // I will investigate it in the future.
238    if let Ok(m) = std::fs::metadata(path).map(FileMetadata::from) {
239      return Ok(m);
240    }
241
242    self.symlink_metadata(path).await?;
243    let path = self.canonicalize(path).await?;
244    std::fs::metadata(path).map(FileMetadata::from)
245  }
246
247  async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
248    std::fs::symlink_metadata(path).map(FileMetadata::from)
249  }
250
251  async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
252    use std::path::Component;
253    let mut path_buf = path.to_path_buf();
254    let link = fs::read_link(&path_buf)?;
255    path_buf.pop();
256    for component in link.components() {
257      match component {
258        Component::ParentDir => {
259          path_buf.pop();
260        }
261        Component::Normal(seg) => {
262          path_buf.push(seg.to_string_lossy().trim_end_matches('\0'));
263        }
264        Component::RootDir => {
265          path_buf = PathBuf::from("/");
266        }
267        Component::CurDir | Component::Prefix(_) => {}
268      }
269
270      // This is not performant, we may optimize it with cache in the future
271      if fs::symlink_metadata(&path_buf).is_ok_and(|m| m.is_symlink()) {
272        let dir = self.canonicalize(&path_buf).await?;
273        path_buf = dir;
274      }
275    }
276    Ok(path_buf)
277  }
278}
279
280#[tokio::test]
281async fn metadata() {
282  let meta = FileMetadata {
283    is_file: true,
284    is_dir: true,
285    is_symlink: true,
286  };
287  assert_eq!(
288    format!("{meta:?}"),
289    "FileMetadata { is_file: true, is_dir: true, is_symlink: true }"
290  );
291  let _ = meta;
292}