diff --git a/cli/factory.rs b/cli/factory.rs index fb08bbcbc08e32..26f3f72a0da17d 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -47,6 +47,7 @@ use deno_runtime::deno_web::BlobStore; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::permissions::RuntimePermissionDescriptorParser; use node_resolver::analyze::NodeCodeTranslator; +use node_resolver::cache::NodeResolutionThreadLocalCache; use once_cell::sync::OnceCell; use sys_traits::EnvCurrentDir; @@ -643,6 +644,7 @@ impl CliFactory { self.workspace_factory()?.clone(), ResolverFactoryOptions { conditions_from_resolution_mode: Default::default(), + node_resolution_cache: Some(Arc::new(NodeResolutionThreadLocalCache)), no_sloppy_imports_cache: false, npm_system_info: self.flags.subcommand.npm_system_info(), specified_import_map: Some(Box::new(CliSpecifiedImportMapProvider { diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 7f544719a7400f..cca9e4caa5d639 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -36,6 +36,8 @@ use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use indexmap::IndexMap; +use node_resolver::cache::NodeResolutionSys; +use node_resolver::cache::NodeResolutionThreadLocalCache; use node_resolver::DenoIsBuiltInNodeModuleChecker; use node_resolver::NodeResolutionKind; use node_resolver::PackageJsonThreadLocalCache; @@ -259,7 +261,7 @@ impl LspScopeResolver { root_node_modules_dir: byonm_npm_resolver .root_node_modules_path() .map(|p| p.to_path_buf()), - sys: factory.sys.clone(), + sys: factory.node_resolution_sys.clone(), pkg_json_resolver: self.pkg_json_resolver.clone(), }, ) @@ -673,6 +675,7 @@ struct ResolverFactoryServices { struct ResolverFactory<'a> { config_data: Option<&'a Arc>, pkg_json_resolver: Arc, + node_resolution_sys: NodeResolutionSys, sys: CliSys, services: ResolverFactoryServices, } @@ -688,6 +691,10 @@ impl<'a> ResolverFactory<'a> { Self { config_data, pkg_json_resolver, + node_resolution_sys: NodeResolutionSys::new( + sys.clone(), + Some(Arc::new(NodeResolutionThreadLocalCache)), + ), sys, services: Default::default(), } @@ -706,7 +713,7 @@ impl<'a> ResolverFactory<'a> { let sys = CliSys::default(); let options = if enable_byonm { CliNpmResolverCreateOptions::Byonm(CliByonmNpmResolverCreateOptions { - sys: self.sys.clone(), + sys: self.node_resolution_sys.clone(), pkg_json_resolver: self.pkg_json_resolver.clone(), root_node_modules_dir: self.config_data.and_then(|config_data| { config_data.node_modules_dir.clone().or_else(|| { @@ -930,7 +937,7 @@ impl<'a> ResolverFactory<'a> { DenoIsBuiltInNodeModuleChecker, npm_resolver.clone(), self.pkg_json_resolver.clone(), - self.sys.clone(), + self.node_resolution_sys.clone(), node_resolver::ConditionsFromResolutionMode::default(), ))) }) diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 0010e046f7767d..d09b461798e0b3 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -49,6 +49,7 @@ use indexmap::IndexMap; use indexmap::IndexSet; use lazy_regex::lazy_regex; use log::error; +use node_resolver::cache::NodeResolutionThreadLocalCache; use node_resolver::ResolutionMode; use once_cell::sync::Lazy; use regex::Captures; @@ -4622,6 +4623,7 @@ fn op_resolve_inner( let state = state.borrow_mut::(); let mark = state.performance.mark_with_args("tsc.op.op_resolve", &args); let referrer = state.specifier_map.normalize(&args.base)?; + NodeResolutionThreadLocalCache::clear(); let specifiers = state .state_snapshot .documents @@ -4640,6 +4642,7 @@ fn op_resolve_inner( }) }) .collect(); + NodeResolutionThreadLocalCache::clear(); state.performance.measure(mark); Ok(specifiers) } diff --git a/cli/rt/run.rs b/cli/rt/run.rs index 53190f24339e13..89f413253b7088 100644 --- a/cli/rt/run.rs +++ b/cli/rt/run.rs @@ -73,6 +73,7 @@ use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerLogLevel; use deno_semver::npm::NpmPackageReqReference; use node_resolver::analyze::NodeCodeTranslator; +use node_resolver::cache::NodeResolutionSys; use node_resolver::errors::ClosestPkgJsonError; use node_resolver::DenoIsBuiltInNodeModuleChecker; use node_resolver::NodeResolutionKind; @@ -689,6 +690,7 @@ pub async fn run( }; NpmRegistryReadPermissionChecker::new(sys.clone(), mode) }; + let node_resolution_sys = NodeResolutionSys::new(sys.clone(), None); let (in_npm_pkg_checker, npm_resolver) = match metadata.node_modules { Some(NodeModules::Managed { node_modules_dir }) => { // create an npmrc that uses the fake npm_registry_url to resolve packages @@ -738,7 +740,7 @@ pub async fn run( DenoInNpmPackageChecker::new(CreateInNpmPkgCheckerOptions::Byonm); let npm_resolver = NpmResolver::::new::( NpmResolverCreateOptions::Byonm(ByonmNpmResolverCreateOptions { - sys: sys.clone(), + sys: node_resolution_sys.clone(), pkg_json_resolver: pkg_json_resolver.clone(), root_node_modules_dir, }), @@ -782,7 +784,7 @@ pub async fn run( DenoIsBuiltInNodeModuleChecker, npm_resolver.clone(), pkg_json_resolver.clone(), - sys.clone(), + node_resolution_sys, node_resolver::ConditionsFromResolutionMode::default(), )); let cjs_tracker = Arc::new(CjsTracker::new( diff --git a/ext/node/ops/require.rs b/ext/node/ops/require.rs index 53f57f0c68f4a0..881158f8232170 100644 --- a/ext/node/ops/require.rs +++ b/ext/node/ops/require.rs @@ -18,6 +18,7 @@ use deno_package_json::PackageJsonRc; use deno_path_util::normalize_path; use deno_path_util::url_from_file_path; use deno_path_util::url_to_file_path; +use node_resolver::cache::NodeResolutionThreadLocalCache; use node_resolver::errors::ClosestPkgJsonError; use node_resolver::InNpmPackageChecker; use node_resolver::NodeResolutionKind; @@ -523,6 +524,8 @@ pub fn op_require_try_self< TSys, >>(); let referrer = UrlOrPathRef::from_path(&pkg.path); + // invalidate the resolution cache in case things have changed + NodeResolutionThreadLocalCache::clear(); let r = node_resolver.package_exports_resolve( &pkg.path, &expansion, @@ -626,6 +629,7 @@ pub fn op_require_resolve_exports< } else { Some(PathBuf::from(parent_path)) }; + NodeResolutionThreadLocalCache::clear(); let r = node_resolver.package_exports_resolve( &pkg.path, &format!(".{expansion}"), @@ -708,6 +712,7 @@ pub fn op_require_package_imports_resolve< TNpmPackageFolderResolver, TSys, >>(); + NodeResolutionThreadLocalCache::clear(); let url = node_resolver.package_imports_resolve( &request, Some(&UrlOrPathRef::from_path(&referrer_path)), diff --git a/resolvers/deno/factory.rs b/resolvers/deno/factory.rs index 95db8415d4798a..b30e6fa060d4c9 100644 --- a/resolvers/deno/factory.rs +++ b/resolvers/deno/factory.rs @@ -23,6 +23,7 @@ use deno_npm::NpmSystemInfo; use deno_path_util::fs::canonicalize_path_maybe_not_exists; use deno_path_util::normalize_path; use futures::future::FutureExt; +use node_resolver::cache::NodeResolutionSys; use node_resolver::ConditionsFromResolutionMode; use node_resolver::DenoIsBuiltInNodeModuleChecker; use node_resolver::NodeResolver; @@ -555,6 +556,7 @@ pub struct ResolverFactoryOptions { pub conditions_from_resolution_mode: ConditionsFromResolutionMode, pub no_sloppy_imports_cache: bool, pub npm_system_info: NpmSystemInfo, + pub node_resolution_cache: Option, pub package_json_cache: Option, pub package_json_dep_resolution: Option, pub specified_import_map: Option>, @@ -584,6 +586,7 @@ pub struct ResolverFactory< + 'static, > { options: ResolverFactoryOptions, + sys: NodeResolutionSys, deno_resolver: async_once_cell::OnceCell>, in_npm_package_checker: Deferred, node_resolver: Deferred< @@ -639,6 +642,10 @@ impl< options: ResolverFactoryOptions, ) -> Self { Self { + sys: NodeResolutionSys::new( + workspace_factory.sys.clone(), + options.node_resolution_cache.clone(), + ), deno_resolver: Default::default(), in_npm_package_checker: Default::default(), node_resolver: Default::default(), @@ -725,7 +732,7 @@ impl< DenoIsBuiltInNodeModuleChecker, self.npm_resolver()?.clone(), self.pkg_json_resolver().clone(), - self.workspace_factory.sys.clone(), + self.sys.clone(), self.options.conditions_from_resolution_mode.clone(), ))) }) @@ -760,7 +767,7 @@ impl< self.npm_resolver.get_or_try_init(|| { Ok(NpmResolver::::new::(if self.use_byonm()? { NpmResolverCreateOptions::Byonm(ByonmNpmResolverCreateOptions { - sys: self.workspace_factory.sys.clone(), + sys: self.sys.clone(), pkg_json_resolver: self.pkg_json_resolver().clone(), root_node_modules_dir: Some( match self.workspace_factory.node_modules_dir_path()? { diff --git a/resolvers/deno/npm/byonm.rs b/resolvers/deno/npm/byonm.rs index a71309c7c313b6..5decc4d82a6ee5 100644 --- a/resolvers/deno/npm/byonm.rs +++ b/resolvers/deno/npm/byonm.rs @@ -11,6 +11,7 @@ use deno_path_util::url_to_file_path; use deno_semver::package::PackageReq; use deno_semver::StackString; use deno_semver::Version; +use node_resolver::cache::NodeResolutionSys; use node_resolver::errors::PackageFolderResolveError; use node_resolver::errors::PackageFolderResolveIoError; use node_resolver::errors::PackageJsonLoadError; @@ -48,7 +49,7 @@ pub enum ByonmResolvePkgFolderFromDenoReqError { pub struct ByonmNpmResolverCreateOptions { // todo(dsherret): investigate removing this pub root_node_modules_dir: Option, - pub sys: TSys, + pub sys: NodeResolutionSys, pub pkg_json_resolver: PackageJsonResolverRc, } @@ -60,7 +61,7 @@ pub type ByonmNpmResolverRc = pub struct ByonmNpmResolver< TSys: FsCanonicalize + FsRead + FsMetadata + FsReadDir, > { - sys: TSys, + sys: NodeResolutionSys, pkg_json_resolver: PackageJsonResolverRc, root_node_modules_dir: Option, } @@ -136,14 +137,14 @@ impl referrer: &Url, ) -> Result { fn node_resolve_dir( - sys: &TSys, + sys: &NodeResolutionSys, alias: &str, start_dir: &Path, ) -> std::io::Result> { for ancestor in start_dir.ancestors() { let node_modules_folder = ancestor.join("node_modules"); let sub_dir = join_package_name(Cow::Owned(node_modules_folder), alias); - if sys.fs_is_dir_no_err(&sub_dir) { + if sys.is_dir(&sub_dir) { return Ok(Some( deno_path_util::fs::canonicalize_path_maybe_not_exists( sys, &sub_dir, @@ -385,7 +386,7 @@ impl referrer: &UrlOrPathRef, ) -> Result { fn inner( - sys: &TSys, + sys: &NodeResolutionSys, name: &str, referrer: &UrlOrPathRef, ) -> Result { @@ -402,7 +403,7 @@ impl }; let sub_dir = join_package_name(node_modules_folder, name); - if sys.fs_is_dir_no_err(&sub_dir) { + if sys.is_dir(&sub_dir) { return Ok(sub_dir); } } diff --git a/resolvers/node/cache.rs b/resolvers/node/cache.rs new file mode 100644 index 00000000000000..a5542742d4b240 --- /dev/null +++ b/resolvers/node/cache.rs @@ -0,0 +1,197 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; + +use sys_traits::BaseFsCanonicalize; +use sys_traits::BaseFsRead; +use sys_traits::BaseFsReadDir; +use sys_traits::FileType; +use sys_traits::FsCanonicalize; +use sys_traits::FsMetadata; +use sys_traits::FsMetadataValue; +use sys_traits::FsRead; +use sys_traits::FsReadDir; + +pub trait NodeResolutionCache: + std::fmt::Debug + crate::sync::MaybeSend + crate::sync::MaybeSync +{ + fn get_canonicalized( + &self, + path: &Path, + ) -> Option>; + fn set_canonicalized(&self, from: PathBuf, to: &std::io::Result); + fn get_file_type(&self, path: &Path) -> Option>; + fn set_file_type(&self, path: PathBuf, value: Option); +} + +thread_local! { + static CANONICALIZED_CACHE: RefCell>> = RefCell::new(HashMap::new()); + static FILE_TYPE_CACHE: RefCell>> = RefCell::new(HashMap::new()); +} + +// We use thread local caches here because it's just more convenient +// and easily allows workers to have separate caches. +#[derive(Debug)] +pub struct NodeResolutionThreadLocalCache; + +impl NodeResolutionThreadLocalCache { + pub fn clear() { + CANONICALIZED_CACHE.with_borrow_mut(|cache| cache.clear()); + FILE_TYPE_CACHE.with_borrow_mut(|cache| cache.clear()); + } +} + +impl NodeResolutionCache for NodeResolutionThreadLocalCache { + fn get_canonicalized( + &self, + path: &Path, + ) -> Option> { + CANONICALIZED_CACHE.with_borrow(|cache| { + let item = cache.get(path)?; + Some(match item { + Some(value) => Ok(value.clone()), + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Not found.", + )), + }) + }) + } + + fn set_canonicalized(&self, from: PathBuf, to: &std::io::Result) { + CANONICALIZED_CACHE.with_borrow_mut(|cache| match to { + Ok(to) => { + cache.insert(from, Some(to.clone())); + } + Err(err) => { + if err.kind() == std::io::ErrorKind::NotFound { + cache.insert(from, None); + } + } + }); + } + + fn get_file_type(&self, path: &Path) -> Option> { + FILE_TYPE_CACHE.with_borrow(|cache| cache.get(path).cloned()) + } + + fn set_file_type(&self, path: PathBuf, value: Option) { + FILE_TYPE_CACHE.with_borrow_mut(|cache| { + cache.insert(path, value); + }) + } +} + +#[allow(clippy::disallowed_types)] +pub type NodeResolutionCacheRc = crate::sync::MaybeArc; + +#[derive(Debug, Default)] +pub struct NodeResolutionSys { + sys: TSys, + cache: Option, +} + +impl Clone for NodeResolutionSys { + fn clone(&self) -> Self { + Self { + sys: self.sys.clone(), + cache: self.cache.clone(), + } + } +} + +impl NodeResolutionSys { + pub fn new(sys: TSys, store: Option) -> Self { + Self { sys, cache: store } + } + + pub fn is_file(&self, path: &Path) -> bool { + match self.get_file_type(path) { + Ok(file_type) => file_type.is_file(), + Err(_) => false, + } + } + + pub fn is_dir(&self, path: &Path) -> bool { + match self.get_file_type(path) { + Ok(file_type) => file_type.is_dir(), + Err(_) => false, + } + } + + pub fn exists_(&self, path: &Path) -> bool { + self.get_file_type(path).is_ok() + } + + pub fn get_file_type(&self, path: &Path) -> std::io::Result { + { + if let Some(maybe_value) = + self.cache.as_ref().and_then(|c| c.get_file_type(path)) + { + return match maybe_value { + Some(value) => Ok(value), + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Not found.", + )), + }; + } + } + match self.sys.fs_metadata(path) { + Ok(metadata) => { + if let Some(cache) = &self.cache { + cache.set_file_type(path.to_path_buf(), Some(metadata.file_type())); + } + Ok(metadata.file_type()) + } + Err(err) => { + if let Some(cache) = &self.cache { + cache.set_file_type(path.to_path_buf(), None); + } + Err(err) + } + } + } +} + +impl BaseFsCanonicalize for NodeResolutionSys { + fn base_fs_canonicalize(&self, from: &Path) -> std::io::Result { + if let Some(cache) = &self.cache { + if let Some(result) = cache.get_canonicalized(from) { + return result; + } + } + let result = self.sys.base_fs_canonicalize(from); + if let Some(cache) = &self.cache { + cache.set_canonicalized(from.to_path_buf(), &result); + } + result + } +} + +impl BaseFsReadDir for NodeResolutionSys { + type ReadDirEntry = TSys::ReadDirEntry; + + #[inline(always)] + fn base_fs_read_dir( + &self, + path: &Path, + ) -> std::io::Result< + Box> + '_>, + > { + self.sys.base_fs_read_dir(path) + } +} + +impl BaseFsRead for NodeResolutionSys { + #[inline(always)] + fn base_fs_read( + &self, + path: &Path, + ) -> std::io::Result> { + self.sys.base_fs_read(path) + } +} diff --git a/resolvers/node/lib.rs b/resolvers/node/lib.rs index a5516f449ae831..6c256e998708b0 100644 --- a/resolvers/node/lib.rs +++ b/resolvers/node/lib.rs @@ -5,6 +5,7 @@ pub mod analyze; mod builtin_modules; +pub mod cache; pub mod errors; mod npm; mod package_json; @@ -16,6 +17,8 @@ mod sync; pub use builtin_modules::DenoIsBuiltInNodeModuleChecker; pub use builtin_modules::IsBuiltInNodeModuleChecker; pub use builtin_modules::DENO_SUPPORTED_BUILTIN_NODE_MODULES; +pub use cache::NodeResolutionCache; +pub use cache::NodeResolutionCacheRc; pub use deno_package_json::PackageJson; pub use npm::InNpmPackageChecker; pub use npm::NpmPackageFolderResolver; diff --git a/resolvers/node/resolution.rs b/resolvers/node/resolution.rs index 273c630b27dd9a..1f6e7117114429 100644 --- a/resolvers/node/resolution.rs +++ b/resolvers/node/resolution.rs @@ -13,10 +13,10 @@ use serde_json::Value; use sys_traits::FileType; use sys_traits::FsCanonicalize; use sys_traits::FsMetadata; -use sys_traits::FsMetadataValue; use sys_traits::FsRead; use url::Url; +use crate::cache::NodeResolutionSys; use crate::errors; use crate::errors::DataUrlReferrerError; use crate::errors::FinalizeResolutionError; @@ -159,7 +159,7 @@ pub struct NodeResolver< is_built_in_node_module_checker: TIsBuiltInNodeModuleChecker, npm_pkg_folder_resolver: TNpmPackageFolderResolver, pkg_json_resolver: PackageJsonResolverRc, - sys: TSys, + sys: NodeResolutionSys, conditions_from_resolution_mode: ConditionsFromResolutionMode, } @@ -181,7 +181,7 @@ impl< is_built_in_node_module_checker: TIsBuiltInNodeModuleChecker, npm_pkg_folder_resolver: TNpmPackageFolderResolver, pkg_json_resolver: PackageJsonResolverRc, - sys: TSys, + sys: NodeResolutionSys, conditions_from_resolution_mode: ConditionsFromResolutionMode, ) -> Self { Self { @@ -372,7 +372,7 @@ impl< path }; - let maybe_file_type = self.sys.fs_metadata(&path).map(|m| m.file_type()); + let maybe_file_type = self.sys.get_file_type(&path); match maybe_file_type { Ok(FileType::Dir) => Err( UnsupportedDirImportError { @@ -494,7 +494,7 @@ impl< conditions: &[&str], ) -> Result { fn probe_extensions( - sys: &TSys, + sys: &NodeResolutionSys, path: &Path, lowercase_path: &str, resolution_mode: ResolutionMode, @@ -503,20 +503,20 @@ impl< let mut searched_for_d_cts = false; if lowercase_path.ends_with(".mjs") { let d_mts_path = with_known_extension(path, "d.mts"); - if sys.fs_is_file_no_err(&d_mts_path) { + if sys.exists_(&d_mts_path) { return Some(d_mts_path); } searched_for_d_mts = true; } else if lowercase_path.ends_with(".cjs") { let d_cts_path = with_known_extension(path, "d.cts"); - if sys.fs_is_file_no_err(&d_cts_path) { + if sys.exists_(&d_cts_path) { return Some(d_cts_path); } searched_for_d_cts = true; } let dts_path = with_known_extension(path, "d.ts"); - if sys.fs_is_file_no_err(&dts_path) { + if sys.exists_(&dts_path) { return Some(dts_path); } @@ -530,7 +530,7 @@ impl< _ => None, // already searched above }; if let Some(specific_dts_path) = specific_dts_path { - if sys.fs_is_file_no_err(&specific_dts_path) { + if sys.exists_(&specific_dts_path) { return Some(specific_dts_path); } } @@ -549,7 +549,7 @@ impl< { return Ok(UrlOrPath::Path(path)); } - if self.sys.fs_is_dir_no_err(&path) { + if self.sys.is_dir(&path) { let resolution_result = self.resolve_package_dir_subpath( &path, /* sub path */ ".", @@ -1467,7 +1467,7 @@ impl< if let Some(main) = maybe_main { let guess = package_json.path.parent().unwrap().join(main).clean(); - if self.sys.fs_is_file_no_err(&guess) { + if self.sys.is_file(&guess) { return Ok(UrlOrPath::Path(guess)); } @@ -1496,7 +1496,7 @@ impl< .unwrap() .join(format!("{main}{ending}")) .clean(); - if self.sys.fs_is_file_no_err(&guess) { + if self.sys.is_file(&guess) { // TODO(bartlomieju): emitLegacyIndexDeprecation() return Ok(UrlOrPath::Path(guess)); } @@ -1533,7 +1533,7 @@ impl< }; for index_file_name in index_file_names { let guess = directory.join(index_file_name).clean(); - if self.sys.fs_is_file_no_err(&guess) { + if self.sys.is_file(&guess) { // TODO(bartlomieju): emitLegacyIndexDeprecation() return Ok(guess); }