From 3227fc43e9b3b0dd99c8c023e127242caf28520b Mon Sep 17 00:00:00 2001 From: Pedro Tacla Yamada Date: Wed, 1 May 2024 13:51:35 +1000 Subject: [PATCH] Use a parcel_filesystem crate (#9682) * Use a parcel_filesystem crate * Move the `parcel_resolver::fs` module into its own crate * Move our in-memory and JSDelegate implementations into that crate * Move the napi helper functions from node-bindings into another crate * Change all usages to use these functions * Fix windows dev-dependencies --- Cargo.lock | 62 ++++--- crates/macros/Cargo.toml | 2 +- crates/node-bindings/Cargo.toml | 9 +- .../node-bindings/src/core/filesystem/mod.rs | 6 - crates/node-bindings/src/core/mod.rs | 1 - .../src/core/requests/config_request/mod.rs | 4 +- crates/node-bindings/src/core/requests/mod.rs | 2 +- crates/node-bindings/src/image.rs | 7 +- crates/parcel_filesystem/Cargo.toml | 17 ++ .../src}/in_memory_file_system/mod.rs | 12 +- .../src}/js_delegate_file_system/mod.rs | 29 ++-- crates/parcel_filesystem/src/lib.rs | 48 ++++++ .../src/os_file_system/canonicalize.rs | 155 ++++++++++++++++++ .../src/os_file_system/mod.rs | 24 +-- crates/parcel_napi_helpers/Cargo.toml | 9 + crates/parcel_napi_helpers/src/lib.rs | 68 ++++++++ packages/utils/node-resolver-rs/Cargo.toml | 1 + packages/utils/node-resolver-rs/src/cache.rs | 2 +- packages/utils/node-resolver-rs/src/lib.rs | 7 +- packages/utils/node-resolver-rs/src/path.rs | 154 ----------------- 20 files changed, 392 insertions(+), 227 deletions(-) delete mode 100644 crates/node-bindings/src/core/filesystem/mod.rs create mode 100644 crates/parcel_filesystem/Cargo.toml rename crates/{node-bindings/src/core/filesystem => parcel_filesystem/src}/in_memory_file_system/mod.rs (95%) rename crates/{node-bindings/src/core/filesystem => parcel_filesystem/src}/js_delegate_file_system/mod.rs (82%) create mode 100644 crates/parcel_filesystem/src/lib.rs create mode 100644 crates/parcel_filesystem/src/os_file_system/canonicalize.rs rename packages/utils/node-resolver-rs/src/fs.rs => crates/parcel_filesystem/src/os_file_system/mod.rs (55%) create mode 100644 crates/parcel_napi_helpers/Cargo.toml create mode 100644 crates/parcel_napi_helpers/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 652f4f8c74b..88c6d014e47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "assert_fs" -version = "1.0.13" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" +checksum = "2cd762e110c8ed629b11b6cde59458cc1c71de78ebbcc30099fc8e0403a2a2ec" dependencies = [ "anstyle", "doc-comment", @@ -467,12 +467,12 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.4.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.12.3", + "hashbrown 0.14.3", "lock_api", "once_cell", "parking_lot_core", @@ -793,11 +793,11 @@ dependencies = [ [[package]] name = "globwalk" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "ignore", "walkdir", ] @@ -1230,9 +1230,9 @@ checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1356,9 +1356,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.14.2" +version = "2.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc1cb00cde484640da9f01a124edbb013576a6ae9843b23857c940936b76d91" +checksum = "da1edd9510299935e4f52a24d1e69ebd224157e3e962c6c847edec5c2e4f786f" dependencies = [ "bitflags 2.3.3", "ctor", @@ -1371,15 +1371,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.0" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b4532cf86bfef556348ac65e561e3123879f0e7566cca6d43a6ff5326f13df" +checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.14.6" +version = "2.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e61bec1ee990ae3e9a5f443484c65fb38e571a898437f0ad283ed69c82fc59c0" +checksum = "e5a6de411b6217dbb47cd7a8c48684b162309ff48a77df9228c082400dd5b030" dependencies = [ "cfg-if", "convert_case", @@ -1391,9 +1391,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.58" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2314f777bc9cde51705d991c44466cee4de4a3f41c6d3d019fcbbebb5cdd47c4" +checksum = "c3e35868d43b178b0eb9c17bd018960b1b5dd1732a7d47c23debe8f5c4caf498" dependencies = [ "convert_case", "once_cell", @@ -1406,9 +1406,9 @@ dependencies = [ [[package]] name = "napi-sys" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" dependencies = [ "libloading", ] @@ -1680,6 +1680,7 @@ dependencies = [ "parcel-js-swc-core", "parcel-macros", "parcel-resolver", + "parcel_filesystem", "rayon", "sentry", "serde", @@ -1703,6 +1704,7 @@ dependencies = [ "itertools 0.10.5", "json_comments", "once_cell", + "parcel_filesystem", "percent-encoding", "serde", "serde_json", @@ -1711,6 +1713,26 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "parcel_filesystem" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_fs", + "dashmap", + "is_elevated", + "napi", + "parcel_napi_helpers", +] + +[[package]] +name = "parcel_napi_helpers" +version = "0.1.0" +dependencies = [ + "anyhow", + "napi", +] + [[package]] name = "parking_lot" version = "0.12.1" diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 19cfefa63b0..bf7818c194f 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -19,5 +19,5 @@ swc_core = { version = "0.89.6", features = [ ] } serde = "1.0.123" napi-derive = { version = "2.12.5", optional = true } -napi = { version = "2.12.6", features = ["serde-json", "napi4", "napi5"], optional = true } +napi = { version = "2.16.4", features = ["serde-json", "napi4", "napi5"], optional = true } crossbeam-channel = { version = "0.5.6", optional = true } diff --git a/crates/node-bindings/Cargo.toml b/crates/node-bindings/Cargo.toml index 7fdd5fe9d5e..ad8cb0c2e38 100644 --- a/crates/node-bindings/Cargo.toml +++ b/crates/node-bindings/Cargo.toml @@ -14,9 +14,10 @@ rustls = ["sentry/rustls"] openssl = ["sentry/native-tls"] [dependencies] -napi-derive = "2.12.5" +napi-derive = "2.16.3" parcel-js-swc-core = { path = "../../packages/transformers/js/core" } parcel-resolver = { path = "../../packages/utils/node-resolver-rs" } +parcel_filesystem = { path = "../parcel_filesystem" } dashmap = "5.4.0" xxhash-rust = { version = "0.8.2", features = ["xxh3"] } log = "0.4.21" @@ -32,7 +33,7 @@ anyhow = "1.0.82" mockall = "0.12.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -napi = { version = "2.12.6", features = ["serde-json", "napi4", "napi5"] } +napi = { version = "2.16.4", features = ["serde-json", "napi4", "napi5"] } parcel-dev-dep-resolver = { path = "../../packages/utils/dev-dep-resolver" } parcel-macros = { path = "../macros", features = ["napi"] } oxipng = "8.0.0" @@ -43,7 +44,7 @@ crossbeam-channel = "0.5.6" indexmap = "1.9.2" [target.'cfg(target_arch = "wasm32")'.dependencies] -napi = { version = "2.12.6", features = ["serde-json"] } +napi = { version = "2.16.4", features = ["serde-json"] } getrandom = { version = "0.2", features = ["custom"], default-features = false } [target.'cfg(target_os = "macos")'.dependencies] @@ -55,4 +56,4 @@ mimalloc = { version = "0.1.25", default-features = false } [dev-dependencies] [build-dependencies] -napi-build = "2" +napi-build = "2.1.3" diff --git a/crates/node-bindings/src/core/filesystem/mod.rs b/crates/node-bindings/src/core/filesystem/mod.rs deleted file mode 100644 index 94686e69686..00000000000 --- a/crates/node-bindings/src/core/filesystem/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// FileSystem implementation that delegates calls to a JS object -pub(crate) mod js_delegate_file_system; - -/// In-memory file-system for testing -#[cfg(test)] -pub(crate) mod in_memory_file_system; diff --git a/crates/node-bindings/src/core/mod.rs b/crates/node-bindings/src/core/mod.rs index 7abd3086ed8..9a926cd401d 100644 --- a/crates/node-bindings/src/core/mod.rs +++ b/crates/node-bindings/src/core/mod.rs @@ -1,5 +1,4 @@ //! Core re-implementation in Rust -mod filesystem; /// Request types and run functions mod requests; diff --git a/crates/node-bindings/src/core/requests/config_request/mod.rs b/crates/node-bindings/src/core/requests/config_request/mod.rs index 430da2c5e7a..7370cb39c0f 100644 --- a/crates/node-bindings/src/core/requests/config_request/mod.rs +++ b/crates/node-bindings/src/core/requests/config_request/mod.rs @@ -155,10 +155,10 @@ struct RequestOptions {} #[cfg(test)] mod test { - use parcel_resolver::OsFileSystem; + use parcel_filesystem::in_memory_file_system::InMemoryFileSystem; + use parcel_filesystem::os_file_system::OsFileSystem; use super::*; - use crate::core::filesystem::in_memory_file_system::InMemoryFileSystem; use crate::core::requests::config_request::run_config_request; use crate::core::requests::request_api::MockRequestApi; diff --git a/crates/node-bindings/src/core/requests/mod.rs b/crates/node-bindings/src/core/requests/mod.rs index 51a3dae94b4..2454c2fa031 100644 --- a/crates/node-bindings/src/core/requests/mod.rs +++ b/crates/node-bindings/src/core/requests/mod.rs @@ -9,9 +9,9 @@ use napi::JsUnknown; use napi::NapiRaw; use napi_derive::napi; -use crate::core::filesystem::js_delegate_file_system::JSDelegateFileSystem; use crate::core::requests::config_request::ConfigRequest; use crate::core::requests::request_api::js_request_api::JSRequestApi; +use parcel_filesystem::js_delegate_file_system::JSDelegateFileSystem; mod config_request; mod request_api; diff --git a/crates/node-bindings/src/image.rs b/crates/node-bindings/src/image.rs index 4b0b2b1bc85..cd723161a0d 100644 --- a/crates/node-bindings/src/image.rs +++ b/crates/node-bindings/src/image.rs @@ -32,7 +32,12 @@ pub fn optimize_image(kind: String, buf: Buffer, env: Env) -> Result { match optimize_jpeg(slice) { Ok(res) => Ok( env - .create_buffer_with_borrowed_data(res.as_ptr(), res.len(), res.as_mut_ptr(), finalize)? + .create_buffer_with_borrowed_data( + res.as_mut_ptr(), + res.len(), + res.as_mut_ptr(), + finalize, + )? .into_raw(), ), Err(err) => { diff --git a/crates/parcel_filesystem/Cargo.toml b/crates/parcel_filesystem/Cargo.toml new file mode 100644 index 00000000000..2d823fad9ce --- /dev/null +++ b/crates/parcel_filesystem/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "parcel_filesystem" +version = "0.1.0" +edition = "2021" +description = "FileSystem wrapper trait for use in Parcel codebase." + +[dependencies] +parcel_napi_helpers = { path = "../parcel_napi_helpers" } +napi = "2.16.4" +dashmap = "5.5.3" +anyhow = "1.0.82" + +[dev-dependencies] +assert_fs = "1.1.1" + +[target.'cfg(windows)'.dev-dependencies] +is_elevated = "0.1.2" diff --git a/crates/node-bindings/src/core/filesystem/in_memory_file_system/mod.rs b/crates/parcel_filesystem/src/in_memory_file_system/mod.rs similarity index 95% rename from crates/node-bindings/src/core/filesystem/in_memory_file_system/mod.rs rename to crates/parcel_filesystem/src/in_memory_file_system/mod.rs index cd2c8ea15d0..eba90ad0a7f 100644 --- a/crates/node-bindings/src/core/filesystem/in_memory_file_system/mod.rs +++ b/crates/parcel_filesystem/src/in_memory_file_system/mod.rs @@ -3,7 +3,7 @@ use std::path::Component; use std::path::Path; use std::path::PathBuf; -use parcel_resolver::FileSystem; +use crate::FileSystem; /// In memory implementation of a file-system entry enum InMemoryFileSystemEntry { @@ -49,11 +49,11 @@ impl Default for InMemoryFileSystem { } impl FileSystem for InMemoryFileSystem { - fn canonicalize>( - &self, - path: P, - _cache: &dashmap::DashMap>, - ) -> std::io::Result { + fn cwd(&self) -> std::io::Result { + Ok(self.current_working_directory.clone()) + } + + fn canonicalize_base>(&self, path: P) -> std::io::Result { let path = path.as_ref(); let mut result = if path.is_absolute() { diff --git a/crates/node-bindings/src/core/filesystem/js_delegate_file_system/mod.rs b/crates/parcel_filesystem/src/js_delegate_file_system/mod.rs similarity index 82% rename from crates/node-bindings/src/core/filesystem/js_delegate_file_system/mod.rs rename to crates/parcel_filesystem/src/js_delegate_file_system/mod.rs index c0eb90c0f70..b0288601274 100644 --- a/crates/node-bindings/src/core/filesystem/js_delegate_file_system/mod.rs +++ b/crates/parcel_filesystem/src/js_delegate_file_system/mod.rs @@ -2,13 +2,13 @@ use std::path::Path; use std::path::PathBuf; use std::rc::Rc; -use dashmap::DashMap; use napi::bindgen_prelude::FromNapiValue; use napi::Env; use napi::JsObject; -use parcel_resolver::FileSystem; +use napi::JsUnknown; +use parcel_napi_helpers::call_method; -use crate::core::requests::call_method; +use crate::FileSystem; /// An implementation of `FileSystem` that delegates calls to a `JsObject`. /// @@ -25,6 +25,12 @@ impl JSDelegateFileSystem { } } +fn path_from_js(result: JsUnknown) -> napi::Result { + let result_string = result.coerce_to_string()?; + let result_string = result_string.into_utf8()?.as_str()?.to_string(); + Ok(PathBuf::from(result_string)) +} + // Convert arbitrary errors to io errors. This is wrong; the `FileSystem` trait should use // `anyhow::Result` fn run_with_errors(block: impl FnOnce() -> anyhow::Result) -> Result { @@ -33,11 +39,14 @@ fn run_with_errors(block: impl FnOnce() -> anyhow::Result) -> Result>( - &self, - path: P, - _cache: &DashMap>, - ) -> std::io::Result { + fn cwd(&self) -> std::io::Result { + run_with_errors(|| { + let result = call_method(&self.env, &self.js_delegate, "cwd", &[])?; + Ok(path_from_js(result)?) + }) + } + + fn canonicalize_base>(&self, path: P) -> std::io::Result { run_with_errors(|| { let path = path.as_ref().to_str().unwrap(); let js_path = self.env.create_string(path)?; @@ -47,9 +56,7 @@ impl FileSystem for JSDelegateFileSystem { "canonicalize", &[&js_path.into_unknown()], )?; - let result_string = result.coerce_to_string()?; - let result_string = result_string.into_utf8()?.as_str()?.to_string(); - Ok(PathBuf::from(result_string)) + Ok(path_from_js(result)?) }) } diff --git a/crates/parcel_filesystem/src/lib.rs b/crates/parcel_filesystem/src/lib.rs new file mode 100644 index 00000000000..67b171403cb --- /dev/null +++ b/crates/parcel_filesystem/src/lib.rs @@ -0,0 +1,48 @@ +use std::io::Result; +use std::path::Path; +use std::path::PathBuf; + +use dashmap::DashMap; + +/// FileSystem implementation that delegates calls to a JS object +pub mod js_delegate_file_system; + +/// In-memory file-system for testing +pub mod in_memory_file_system; + +/// File-system implementation using std::fs and a canonicalize cache +pub mod os_file_system; + +/// Trait abstracting file-system operations +/// . +/// +/// ## TODO list +/// +/// * [ ] Do not leak dash-map cache into calls. Instead this should be managed by implementations; +/// it should not be in the trait +/// * [ ] Do not use io results, instead use anyhow or this error +/// +pub trait FileSystem { + fn cwd(&self) -> Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Not implemented", + )) + } + fn canonicalize_base>(&self, _path: P) -> Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Not implemented", + )) + } + fn canonicalize>( + &self, + path: P, + _cache: &DashMap>, + ) -> Result { + self.canonicalize_base(path) + } + fn read_to_string>(&self, path: P) -> Result; + fn is_file>(&self, path: P) -> bool; + fn is_dir>(&self, path: P) -> bool; +} diff --git a/crates/parcel_filesystem/src/os_file_system/canonicalize.rs b/crates/parcel_filesystem/src/os_file_system/canonicalize.rs new file mode 100644 index 00000000000..84f9ce2fc30 --- /dev/null +++ b/crates/parcel_filesystem/src/os_file_system/canonicalize.rs @@ -0,0 +1,155 @@ +use std::collections::VecDeque; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +use dashmap::DashMap; + +#[cfg(not(target_arch = "wasm32"))] +/// A reimplementation of std::fs::canonicalize with intermediary caching. +pub fn canonicalize( + path: &Path, + cache: &DashMap>, +) -> std::io::Result { + let mut ret = PathBuf::new(); + let mut seen_links = 0; + let mut queue = VecDeque::new(); + + queue.push_back(path.to_path_buf()); + + while let Some(cur_path) = queue.pop_front() { + let mut components = cur_path.components(); + for component in &mut components { + match component { + Component::Prefix(c) => ret.push(c.as_os_str()), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + + // First, check the cache for the path up to this point. + let link = if let Some(cached) = cache.get(&ret) { + if let Some(link) = &*cached { + link.clone() + } else { + continue; + } + } else { + let stat = std::fs::symlink_metadata(&ret)?; + if !stat.is_symlink() { + cache.insert(ret.clone(), None); + continue; + } + + let link = std::fs::read_link(&ret)?; + cache.insert(ret.clone(), Some(link.clone())); + link + }; + + seen_links += 1; + if seen_links > 32 { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Too many symlinks", + )); + } + + // If the link is absolute, replace the result path + // with it, otherwise remove the last segment and + // resolve the link components next. + if link.is_absolute() { + ret = PathBuf::new(); + } else { + ret.pop(); + } + + let remaining = components.as_path(); + if !remaining.as_os_str().is_empty() { + queue.push_front(remaining.to_path_buf()); + } + queue.push_front(link); + break; + } + } + } + } + + Ok(ret) +} + +#[cfg(test)] +mod test { + use assert_fs::prelude::*; + + use super::*; + + #[test] + fn test_canonicalize() -> Result<(), Box> { + #[cfg(windows)] + if !is_elevated::is_elevated() { + println!("skipping symlink tests due to missing permissions"); + return Ok(()); + } + + let dir = assert_fs::TempDir::new()?; + dir.child("foo/bar.js").write_str("")?; + dir.child("root.js").write_str("")?; + + dir + .child("symlink") + .symlink_to_file(Path::new("foo").join("bar.js"))?; + dir + .child("foo/symlink") + .symlink_to_file(Path::new("..").join("root.js"))?; + dir + .child("absolute") + .symlink_to_file(dir.child("root.js").path())?; + dir + .child("recursive") + .symlink_to_file(Path::new("foo").join("symlink"))?; + dir.child("cycle").symlink_to_file("cycle1")?; + dir.child("cycle1").symlink_to_file("cycle")?; + dir.child("a/b/c").create_dir_all()?; + dir.child("a/b/e").symlink_to_file("..")?; + dir.child("a/d").symlink_to_file("..")?; + dir.child("a/b/c/x.txt").write_str("")?; + dir + .child("a/link") + .symlink_to_file(dir.child("a/b").path())?; + + let cache = DashMap::new(); + + assert_eq!( + canonicalize(dir.child("symlink").path(), &cache)?, + canonicalize(dir.child("foo/bar.js").path(), &cache)? + ); + assert_eq!( + canonicalize(dir.child("foo/symlink").path(), &cache)?, + canonicalize(dir.child("root.js").path(), &cache)? + ); + assert_eq!( + canonicalize(dir.child("absolute").path(), &cache)?, + canonicalize(dir.child("root.js").path(), &cache)? + ); + assert_eq!( + canonicalize(dir.child("recursive").path(), &cache)?, + canonicalize(dir.child("root.js").path(), &cache)? + ); + assert!(canonicalize(dir.child("cycle").path(), &cache).is_err()); + assert_eq!( + canonicalize(dir.child("a/b/e/d/a/b/e/d/a").path(), &cache)?, + canonicalize(dir.child("a").path(), &cache)? + ); + assert_eq!( + canonicalize(dir.child("a/link/c/x.txt").path(), &cache)?, + canonicalize(dir.child("a/b/c/x.txt").path(), &cache)? + ); + + Ok(()) + } +} diff --git a/packages/utils/node-resolver-rs/src/fs.rs b/crates/parcel_filesystem/src/os_file_system/mod.rs similarity index 55% rename from packages/utils/node-resolver-rs/src/fs.rs rename to crates/parcel_filesystem/src/os_file_system/mod.rs index 9820f756f6b..4d0d846a636 100644 --- a/packages/utils/node-resolver-rs/src/fs.rs +++ b/crates/parcel_filesystem/src/os_file_system/mod.rs @@ -1,22 +1,12 @@ -use std::io::Result; use std::path::Path; use std::path::PathBuf; +use canonicalize::canonicalize; use dashmap::DashMap; -#[cfg(not(target_arch = "wasm32"))] -use crate::path::canonicalize; +use crate::FileSystem; -pub trait FileSystem { - fn canonicalize>( - &self, - path: P, - cache: &DashMap>, - ) -> Result; - fn read_to_string>(&self, path: P) -> Result; - fn is_file>(&self, path: P) -> bool; - fn is_dir>(&self, path: P) -> bool; -} +mod canonicalize; #[cfg(not(target_arch = "wasm32"))] #[derive(Default)] @@ -24,15 +14,19 @@ pub struct OsFileSystem; #[cfg(not(target_arch = "wasm32"))] impl FileSystem for OsFileSystem { + fn cwd(&self) -> std::io::Result { + std::env::current_dir() + } + fn canonicalize>( &self, path: P, cache: &DashMap>, - ) -> Result { + ) -> std::io::Result { canonicalize(path.as_ref(), cache) } - fn read_to_string>(&self, path: P) -> Result { + fn read_to_string>(&self, path: P) -> std::io::Result { std::fs::read_to_string(path) } diff --git a/crates/parcel_napi_helpers/Cargo.toml b/crates/parcel_napi_helpers/Cargo.toml new file mode 100644 index 00000000000..9e096f71b45 --- /dev/null +++ b/crates/parcel_napi_helpers/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "parcel_napi_helpers" +version = "0.1.0" +edition = "2021" +description = "napi helpers for JS interop" + +[dependencies] +anyhow = "1" +napi = "2.16.4" diff --git a/crates/parcel_napi_helpers/src/lib.rs b/crates/parcel_napi_helpers/src/lib.rs new file mode 100644 index 00000000000..8f6b4916691 --- /dev/null +++ b/crates/parcel_napi_helpers/src/lib.rs @@ -0,0 +1,68 @@ +use napi::bindgen_prelude::FromNapiValue; +use napi::Env; +use napi::JsFunction; +use napi::JsObject; +use napi::JsUnknown; +use napi::NapiRaw; + +/// Convert anyhow error to napi error +pub fn anyhow_napi(value: anyhow::Error) -> napi::Error { + napi::Error::from_reason(format!("[napi] {}", value.to_string())) +} + +/// Get an object field as a JSFunction. Will error out if the field is not present or isn't an +/// instance of the global `"Function"`. +/// +/// ## Safety +/// Uses raw NAPI casts, but checks that object field is a function +pub fn get_function(env: &Env, js_object: &JsObject, field_name: &str) -> napi::Result { + let Some(method): Option = js_object.get(field_name)? else { + return Err(napi::Error::from_reason(format!( + "[napi] Method not found: {}", + field_name + ))); + }; + let function_class: JsUnknown = env.get_global()?.get_named_property("Function")?; + let is_function = method.instanceof(function_class)?; + if !is_function { + return Err(napi::Error::from_reason(format!( + "[napi] Method is not a function: {}", + field_name + ))); + } + + let method_fn = unsafe { JsFunction::from_napi_value(env.raw(), method.raw()) }?; + Ok(method_fn) +} + +/// Call a method on an object with a set of arguments. +/// +/// Will error out if the method doesn't exist or if the field is not a function. +/// +/// This does some redundant work ; so you may want to call `get_function` +/// directly if calling a method on a loop. +/// +/// The function takes `JsUnknown` references so any type can be used as an +/// argument. +/// +/// ## Safety +/// Uses raw NAPI casts, but checks that object field is a function +/// +/// ## Example +/// ```skip +/// let string_parameter = env.create_string(path.to_str().unwrap())?; +/// let args = [&string_parameter.into_unknown()]; +/// let field_name = "method"; +/// +/// call_method(&self.env, &js_object, field_name, &args)?; +/// ``` +pub fn call_method( + env: &Env, + js_object: &JsObject, + field_name: &str, + args: &[&JsUnknown], +) -> napi::Result { + let method_fn = get_function(env, js_object, field_name)?; + let result = method_fn.call(Some(&js_object), &args)?; + Ok(result) +} diff --git a/packages/utils/node-resolver-rs/Cargo.toml b/packages/utils/node-resolver-rs/Cargo.toml index 2d280033208..41bd302fefa 100644 --- a/packages/utils/node-resolver-rs/Cargo.toml +++ b/packages/utils/node-resolver-rs/Cargo.toml @@ -14,6 +14,7 @@ bitflags = "1.3.2" indexmap = { version = "1.9.2", features = ["serde"] } itertools = "0.10.5" json_comments = { path = "../../../crates/json-comments-rs" } +parcel_filesystem = { path = "../../../crates/parcel_filesystem" } typed-arena = "2.0.2" elsa = "1.7.0" once_cell = "1.17.0" diff --git a/packages/utils/node-resolver-rs/src/cache.rs b/packages/utils/node-resolver-rs/src/cache.rs index 110bb34bc29..38c6d405471 100644 --- a/packages/utils/node-resolver-rs/src/cache.rs +++ b/packages/utils/node-resolver-rs/src/cache.rs @@ -6,9 +6,9 @@ use std::sync::Mutex; use dashmap::DashMap; use elsa::sync::FrozenMap; +use parcel_filesystem::FileSystem; use typed_arena::Arena; -use crate::fs::FileSystem; use crate::package_json::PackageJson; use crate::package_json::SourceField; use crate::tsconfig::TsConfig; diff --git a/packages/utils/node-resolver-rs/src/lib.rs b/packages/utils/node-resolver-rs/src/lib.rs index e0537e5ce24..8af1691e8ef 100644 --- a/packages/utils/node-resolver-rs/src/lib.rs +++ b/packages/utils/node-resolver-rs/src/lib.rs @@ -16,7 +16,6 @@ use tsconfig::TsConfig; mod builtins; mod cache; mod error; -mod fs; mod invalidations; mod package_json; mod path; @@ -27,14 +26,14 @@ mod url_to_path; pub use cache::Cache; pub use cache::CacheCow; pub use error::ResolverError; -pub use fs::FileSystem; -#[cfg(not(target_arch = "wasm32"))] -pub use fs::OsFileSystem; pub use invalidations::*; pub use package_json::ExportsCondition; pub use package_json::Fields; pub use package_json::ModuleType; pub use package_json::PackageJsonError; +#[cfg(not(target_arch = "wasm32"))] +pub use parcel_filesystem::os_file_system::OsFileSystem; +pub use parcel_filesystem::FileSystem; pub use specifier::Specifier; pub use specifier::SpecifierError; pub use specifier::SpecifierType; diff --git a/packages/utils/node-resolver-rs/src/path.rs b/packages/utils/node-resolver-rs/src/path.rs index 554e6ab6408..4e27412c3cf 100644 --- a/packages/utils/node-resolver-rs/src/path.rs +++ b/packages/utils/node-resolver-rs/src/path.rs @@ -1,12 +1,7 @@ -#[cfg(not(target_arch = "wasm32"))] -use std::collections::VecDeque; use std::path::Component; use std::path::Path; use std::path::PathBuf; -#[cfg(not(target_arch = "wasm32"))] -use dashmap::DashMap; - pub fn normalize_path(path: &Path) -> PathBuf { // Normalize path components to resolve ".." and "." segments. // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 @@ -61,152 +56,3 @@ pub fn resolve_path, B: AsRef>(base: A, subpath: B) -> Path ret } - -#[cfg(not(target_arch = "wasm32"))] -/// A reimplementation of std::fs::canonicalize with intermediary caching. -pub fn canonicalize( - path: &Path, - cache: &DashMap>, -) -> std::io::Result { - let mut ret = PathBuf::new(); - let mut seen_links = 0; - let mut queue = VecDeque::new(); - - queue.push_back(path.to_path_buf()); - - while let Some(cur_path) = queue.pop_front() { - let mut components = cur_path.components(); - for component in &mut components { - match component { - Component::Prefix(c) => ret.push(c.as_os_str()), - Component::RootDir => { - ret.push(component.as_os_str()); - } - Component::CurDir => {} - Component::ParentDir => { - ret.pop(); - } - Component::Normal(c) => { - ret.push(c); - - // First, check the cache for the path up to this point. - let link = if let Some(cached) = cache.get(&ret) { - if let Some(link) = &*cached { - link.clone() - } else { - continue; - } - } else { - let stat = std::fs::symlink_metadata(&ret)?; - if !stat.is_symlink() { - cache.insert(ret.clone(), None); - continue; - } - - let link = std::fs::read_link(&ret)?; - cache.insert(ret.clone(), Some(link.clone())); - link - }; - - seen_links += 1; - if seen_links > 32 { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Too many symlinks", - )); - } - - // If the link is absolute, replace the result path - // with it, otherwise remove the last segment and - // resolve the link components next. - if link.is_absolute() { - ret = PathBuf::new(); - } else { - ret.pop(); - } - - let remaining = components.as_path(); - if !remaining.as_os_str().is_empty() { - queue.push_front(remaining.to_path_buf()); - } - queue.push_front(link); - break; - } - } - } - } - - Ok(ret) -} - -#[cfg(test)] -mod test { - use assert_fs::prelude::*; - - use super::*; - - #[test] - fn test_canonicalize() -> Result<(), Box> { - #[cfg(windows)] - if !is_elevated::is_elevated() { - println!("skipping symlink tests due to missing permissions"); - return Ok(()); - } - - let dir = assert_fs::TempDir::new()?; - dir.child("foo/bar.js").write_str("")?; - dir.child("root.js").write_str("")?; - - dir - .child("symlink") - .symlink_to_file(Path::new("foo").join("bar.js"))?; - dir - .child("foo/symlink") - .symlink_to_file(Path::new("..").join("root.js"))?; - dir - .child("absolute") - .symlink_to_file(dir.child("root.js").path())?; - dir - .child("recursive") - .symlink_to_file(Path::new("foo").join("symlink"))?; - dir.child("cycle").symlink_to_file("cycle1")?; - dir.child("cycle1").symlink_to_file("cycle")?; - dir.child("a/b/c").create_dir_all()?; - dir.child("a/b/e").symlink_to_file("..")?; - dir.child("a/d").symlink_to_file("..")?; - dir.child("a/b/c/x.txt").write_str("")?; - dir - .child("a/link") - .symlink_to_file(dir.child("a/b").path())?; - - let cache = DashMap::new(); - - assert_eq!( - canonicalize(dir.child("symlink").path(), &cache)?, - canonicalize(dir.child("foo/bar.js").path(), &cache)? - ); - assert_eq!( - canonicalize(dir.child("foo/symlink").path(), &cache)?, - canonicalize(dir.child("root.js").path(), &cache)? - ); - assert_eq!( - canonicalize(dir.child("absolute").path(), &cache)?, - canonicalize(dir.child("root.js").path(), &cache)? - ); - assert_eq!( - canonicalize(dir.child("recursive").path(), &cache)?, - canonicalize(dir.child("root.js").path(), &cache)? - ); - assert!(canonicalize(dir.child("cycle").path(), &cache).is_err()); - assert_eq!( - canonicalize(dir.child("a/b/e/d/a/b/e/d/a").path(), &cache)?, - canonicalize(dir.child("a").path(), &cache)? - ); - assert_eq!( - canonicalize(dir.child("a/link/c/x.txt").path(), &cache)?, - canonicalize(dir.child("a/b/c/x.txt").path(), &cache)? - ); - - Ok(()) - } -}