rspack_resolver/
file_system.rs1use 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#[async_trait::async_trait]
12pub trait FileSystem {
13 async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
19 async fn read_to_string(&self, path: &Path) -> io::Result<String>;
30
31 async fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
41
42 async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
53
54 async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
65}
66
67#[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
116pub 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 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 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}