diff --git a/Cargo.lock b/Cargo.lock index 1cee52dc..fc520936 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -799,6 +799,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "memchr" version = "2.7.6" @@ -950,6 +959,7 @@ dependencies = [ "json-strip-comments", "once_cell", "papaya", + "parking_lot", "pico-args", "pnp", "rayon", @@ -992,6 +1002,29 @@ dependencies = [ "seize", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -1200,6 +1233,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "seize" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 20128837..1facff86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ indexmap = { version = "2", features = ["serde"] } json-strip-comments = "3" once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable. papaya = "0.2" +parking_lot = "0.12" rustc-hash = { version = "2" } serde = { version = "1", features = ["derive"] } # derive for Deserialize from package.json serde_json = { version = "1", features = ["preserve_order"] } # preserve_order: package_json.exports requires order such as `["require", "import", "default"]` diff --git a/src/cache/cache_impl.rs b/src/cache/cache_impl.rs index 8d63f7c7..4f701358 100644 --- a/src/cache/cache_impl.rs +++ b/src/cache/cache_impl.rs @@ -11,6 +11,7 @@ use cfg_if::cfg_if; #[cfg(feature = "yarn_pnp")] use once_cell::sync::OnceCell; use papaya::{HashMap, HashSet}; +use parking_lot::RwLock; use rustc_hash::FxHasher; use super::borrowed_path::BorrowedCachedPath; @@ -21,11 +22,14 @@ use crate::{ context::ResolveContext as Ctx, path::PathUtil, }; +pub type PackageJsonIndex = usize; + /// Cache implementation used for caching filesystem access. #[derive(Default)] pub struct Cache { pub(crate) fs: Fs, pub(crate) paths: HashSet>, + pub(crate) package_jsons: RwLock>>, pub(crate) tsconfigs: HashMap, BuildHasherDefault>, #[cfg(feature = "yarn_pnp")] pub(crate) yarn_pnp_manifest: OnceCell, @@ -35,6 +39,7 @@ impl Cache { pub fn clear(&self) { self.paths.pin().clear(); self.tsconfigs.pin().clear(); + self.package_jsons.write().clear(); } #[allow(clippy::cast_possible_truncation)] @@ -110,38 +115,48 @@ impl Cache { .get_or_try_init(|| { let package_json_path = path.path.join("package.json"); let Ok(package_json_bytes) = self.fs.read(&package_json_path) else { + if let Some(deps) = &mut ctx.missing_dependencies { + deps.push(package_json_path); + } return Ok(None); }; - let real_path = if options.symlinks { self.canonicalize(path)?.join("package.json") } else { package_json_path.clone() }; - PackageJson::parse(&self.fs, package_json_path, real_path, package_json_bytes) - .map(|package_json| Some(Arc::new(package_json))) - .map_err(ResolveError::Json) + PackageJson::parse( + &self.fs, + package_json_path.clone(), + real_path, + package_json_bytes, + ) + .map(|package_json| { + let arc = Arc::new(package_json); + let index = { + let mut arena = self.package_jsons.write(); + let index = arena.len(); + arena.push(arc); + index + }; + Some(index) + }) + .map_err(ResolveError::Json) + // https://github.com/webpack/enhanced-resolve/blob/58464fc7cb56673c9aa849e68e6300239601e615/lib/DescriptionFileUtils.js#L68-L82 + .inspect(|_| { + ctx.add_file_dependency(&package_json_path); + }) + .inspect_err(|_| { + if let Some(deps) = &mut ctx.file_dependencies { + deps.push(package_json_path.clone()); + } + }) }) .cloned(); - // https://github.com/webpack/enhanced-resolve/blob/58464fc7cb56673c9aa849e68e6300239601e615/lib/DescriptionFileUtils.js#L68-L82 - match &result { - Ok(Some(package_json)) => { - ctx.add_file_dependency(&package_json.path); - } - Ok(None) => { - // Avoid an allocation by making this lazy - if let Some(deps) = &mut ctx.missing_dependencies { - deps.push(path.path.join("package.json")); - } - } - Err(_) => { - if let Some(deps) = &mut ctx.file_dependencies { - deps.push(path.path.join("package.json")); - } - } - } - result + result.map(|option_index| { + option_index.and_then(|index| self.package_jsons.read().get(index).cloned()) + }) } pub(crate) fn get_tsconfig Result<(), ResolveError>>( @@ -213,6 +228,7 @@ impl Cache { .hasher(BuildHasherDefault::default()) .resize_mode(papaya::ResizeMode::Blocking) .build(), + package_jsons: RwLock::new(Vec::with_capacity(512)), tsconfigs: HashMap::builder() .hasher(BuildHasherDefault::default()) .resize_mode(papaya::ResizeMode::Blocking) diff --git a/src/cache/cached_path.rs b/src/cache/cached_path.rs index 3222798f..962a2d9c 100644 --- a/src/cache/cached_path.rs +++ b/src/cache/cached_path.rs @@ -11,6 +11,7 @@ use cfg_if::cfg_if; use once_cell::sync::OnceCell as OnceLock; use super::cache_impl::Cache; +use super::cache_impl::PackageJsonIndex; use super::thread_local::SCRATCH_PATH; use crate::{ FileSystem, PackageJson, ResolveError, ResolveOptions, TsConfig, context::ResolveContext as Ctx, @@ -28,7 +29,7 @@ pub struct CachedPathImpl { pub meta: OnceLock>, // None means not found. pub canonicalized: OnceLock>, pub node_modules: OnceLock>>, - pub package_json: OnceLock>>, + pub package_json: OnceLock>, pub tsconfig: OnceLock>>, } diff --git a/src/package_json/mod.rs b/src/package_json/mod.rs index 9377b84d..4c867420 100644 --- a/src/package_json/mod.rs +++ b/src/package_json/mod.rs @@ -14,15 +14,20 @@ pub use serde::*; #[cfg(target_endian = "little")] pub use simd::*; -use std::{fmt, path::PathBuf}; +use std::{fmt, path::Path}; use crate::JSONError; /// Check if JSON content is empty or contains only whitespace -fn check_if_empty(json_bytes: &[u8], path: PathBuf) -> Result<(), JSONError> { +fn check_if_empty(json_bytes: &[u8], path: &Path) -> Result<(), JSONError> { // Check if content is empty or whitespace-only if json_bytes.iter().all(|&b| b.is_ascii_whitespace()) { - return Err(JSONError { path, message: "File is empty".to_string(), line: 0, column: 0 }); + return Err(JSONError { + path: path.to_path_buf(), + message: "File is empty".to_string(), + line: 0, + column: 0, + }); } Ok(()) } diff --git a/src/package_json/serde.rs b/src/package_json/serde.rs index 17f87cd5..73291c89 100644 --- a/src/package_json/serde.rs +++ b/src/package_json/serde.rs @@ -230,7 +230,7 @@ impl PackageJson { let json_bytes = if json.starts_with(b"\xEF\xBB\xBF") { &json[3..] } else { &json[..] }; // Check if empty after BOM stripping - super::check_if_empty(json_bytes, path.clone())?; + super::check_if_empty(json_bytes, &path)?; // Parse JSON directly from bytes let value = serde_json::from_slice::(json_bytes).map_err(|error| JSONError { diff --git a/src/package_json/simd.rs b/src/package_json/simd.rs index 92b8c7da..373c643d 100644 --- a/src/package_json/simd.rs +++ b/src/package_json/simd.rs @@ -269,7 +269,7 @@ impl PackageJson { } // Check if empty after BOM stripping - super::check_if_empty(&json_bytes, path.clone())?; + super::check_if_empty(&json_bytes, &path)?; // Create the self-cell with the JSON bytes and parsed BorrowedValue let cell = PackageJsonCell::try_new(MutBorrow::new(json_bytes), |bytes| { diff --git a/src/tests/dependencies.rs b/src/tests/dependencies.rs index ee4e441e..02f0d946 100644 --- a/src/tests/dependencies.rs +++ b/src/tests/dependencies.rs @@ -1,7 +1,7 @@ //! https://github.com/webpack/enhanced-resolve/blob/main/test/dependencies.test.js #[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows. -mod windows { +mod test { use std::path::PathBuf; use super::super::memory_fs::MemoryFS; @@ -18,17 +18,6 @@ mod windows { #[test] fn test() { - let file_system = file_system(); - - let resolver = ResolverGeneric::new_with_file_system( - file_system, - ResolveOptions { - extensions: vec![".json".into(), ".js".into()], - modules: vec!["/modules".into(), "node_modules".into()], - ..ResolveOptions::default() - }, - ); - let data = [ ( "middle module request", @@ -92,6 +81,15 @@ mod windows { ]; for (name, context, request, result, file_dependencies, missing_dependencies) in data { + let file_system = file_system(); + let resolver = ResolverGeneric::new_with_file_system( + file_system, + ResolveOptions { + extensions: vec![".json".into(), ".js".into()], + modules: vec!["/modules".into(), "node_modules".into()], + ..ResolveOptions::default() + }, + ); let mut ctx = ResolveContext::default(); let path = PathBuf::from(context); let resolved_path =