diff --git a/Cargo.lock b/Cargo.lock index 2c25c0de03..87265c02d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2007,6 +2007,8 @@ version = "0.9.0" dependencies = [ "anyhow", "base32", + "dashmap", + "deno_config", "deno_media_type", "deno_package_json", "deno_path_util", diff --git a/Cargo.toml b/Cargo.toml index f384b92553..4a78e7e466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ deno_ast = { version = "=0.43.3", features = ["transpiling"] } deno_core = { version = "0.319.0" } deno_bench_util = { version = "0.171.0", path = "./bench_util" } +deno_config = { version = "=0.38.2", features = ["workspace", "sync"] } deno_lockfile = "=0.23.1" deno_media_type = { version = "0.2.0", features = ["module_specifier"] } deno_npm = "=0.25.4" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 928259a835..374f3dae5e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -69,8 +69,8 @@ winres.workspace = true [dependencies] deno_ast = { workspace = true, features = ["bundler", "cjs", "codegen", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] } -deno_cache_dir = { workspace = true } -deno_config = { version = "=0.38.2", features = ["workspace", "sync"] } +deno_cache_dir.workspace = true +deno_config.workspace = true deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } deno_doc = { version = "0.156.0", default-features = false, features = ["rust", "html", "syntect"] } deno_graph = { version = "=0.84.1" } diff --git a/cli/factory.rs b/cli/factory.rs index 5cb2dd7b3a..7949a83a55 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -42,8 +42,9 @@ use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; use crate::npm::CreateInNpmPkgCheckerOptions; use crate::resolver::CjsTracker; +use crate::resolver::CliDenoResolver; use crate::resolver::CliDenoResolverFs; -use crate::resolver::CliNodeResolver; +use crate::resolver::CliNpmReqResolver; use crate::resolver::CliResolver; use crate::resolver::CliResolverOptions; use crate::resolver::CliSloppyImportsResolver; @@ -71,6 +72,9 @@ use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::FeatureChecker; +use deno_resolver::npm::NpmReqResolverOptions; +use deno_resolver::DenoResolverOptions; +use deno_resolver::NodeAndNpmReqResolver; use deno_runtime::deno_fs; use deno_runtime::deno_node::DenoFsNodeResolverEnv; use deno_runtime::deno_node::NodeResolver; @@ -126,7 +130,7 @@ impl RootCertStoreProvider for CliRootCertStoreProvider { } } -struct Deferred(once_cell::unsync::OnceCell); +pub struct Deferred(once_cell::unsync::OnceCell); impl Default for Deferred { fn default() -> Self { @@ -175,9 +179,9 @@ struct CliFactoryServices { blob_store: Deferred>, caches: Deferred>, cjs_tracker: Deferred>, - cli_node_resolver: Deferred>, cli_options: Deferred>, code_cache: Deferred>, + deno_resolver: Deferred>, emit_cache: Deferred>, emitter: Deferred>, feature_checker: Deferred>, @@ -197,6 +201,7 @@ struct CliFactoryServices { node_code_translator: Deferred>, node_resolver: Deferred>, npm_cache_dir: Deferred>, + npm_req_resolver: Deferred>, npm_resolver: Deferred>, parsed_source_cache: Deferred>, permission_desc_parser: Deferred>, @@ -523,6 +528,31 @@ impl CliFactory { .await } + pub async fn deno_resolver(&self) -> Result<&Arc, AnyError> { + self + .services + .deno_resolver + .get_or_try_init_async(async { + let cli_options = self.cli_options()?; + Ok(Arc::new(CliDenoResolver::new(DenoResolverOptions { + in_npm_pkg_checker: self.in_npm_pkg_checker()?.clone(), + node_and_req_resolver: if cli_options.no_npm() { + None + } else { + Some(NodeAndNpmReqResolver { + node_resolver: self.node_resolver().await?.clone(), + npm_req_resolver: self.npm_req_resolver().await?.clone(), + }) + }, + sloppy_imports_resolver: self.sloppy_imports_resolver()?.cloned(), + workspace_resolver: self.workspace_resolver().await?.clone(), + is_byonm: cli_options.use_byonm(), + maybe_vendor_dir: cli_options.vendor_dir_path(), + }))) + }) + .await + } + pub async fn resolver(&self) -> Result<&Arc, AnyError> { self .services @@ -531,17 +561,14 @@ impl CliFactory { async { let cli_options = self.cli_options()?; Ok(Arc::new(CliResolver::new(CliResolverOptions { - sloppy_imports_resolver: self.sloppy_imports_resolver()?.cloned(), - node_resolver: Some(self.cli_node_resolver().await?.clone()), npm_resolver: if cli_options.no_npm() { None } else { Some(self.npm_resolver().await?.clone()) }, - workspace_resolver: self.workspace_resolver().await?.clone(), bare_node_builtins_enabled: cli_options .unstable_bare_node_builtins(), - maybe_vendor_dir: cli_options.vendor_dir_path(), + deno_resolver: self.deno_resolver().await?.clone(), }))) } .boxed_local(), @@ -624,7 +651,11 @@ impl CliFactory { Ok(Arc::new(NodeResolver::new( DenoFsNodeResolverEnv::new(self.fs().clone()), self.in_npm_pkg_checker()?.clone(), - self.npm_resolver().await?.clone().into_npm_resolver(), + self + .npm_resolver() + .await? + .clone() + .into_npm_pkg_folder_resolver(), self.pkg_json_resolver().clone(), ))) } @@ -656,13 +687,36 @@ impl CliFactory { DenoFsNodeResolverEnv::new(self.fs().clone()), self.in_npm_pkg_checker()?.clone(), node_resolver, - self.npm_resolver().await?.clone().into_npm_resolver(), + self + .npm_resolver() + .await? + .clone() + .into_npm_pkg_folder_resolver(), self.pkg_json_resolver().clone(), ))) }) .await } + pub async fn npm_req_resolver( + &self, + ) -> Result<&Arc, AnyError> { + self + .services + .npm_req_resolver + .get_or_try_init_async(async { + let npm_resolver = self.npm_resolver().await?; + Ok(Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { + byonm_resolver: (npm_resolver.clone()).into_maybe_byonm(), + fs: CliDenoResolverFs(self.fs().clone()), + in_npm_pkg_checker: self.in_npm_pkg_checker()?.clone(), + node_resolver: self.node_resolver().await?.clone(), + npm_req_resolver: npm_resolver.clone().into_npm_req_resolver(), + }))) + }) + .await + } + pub fn pkg_json_resolver(&self) -> &Arc { self.services.pkg_json_resolver.get_or_init(|| { Arc::new(PackageJsonResolver::new(DenoFsNodeResolverEnv::new( @@ -799,23 +853,6 @@ impl CliFactory { }) } - pub async fn cli_node_resolver( - &self, - ) -> Result<&Arc, AnyError> { - self - .services - .cli_node_resolver - .get_or_try_init_async(async { - Ok(Arc::new(CliNodeResolver::new( - self.fs().clone(), - self.in_npm_pkg_checker()?.clone(), - self.node_resolver().await?.clone(), - self.npm_resolver().await?.clone(), - ))) - }) - .await - } - pub fn permission_desc_parser( &self, ) -> Result<&Arc, AnyError> { @@ -880,7 +917,6 @@ impl CliFactory { let fs = self.fs(); let node_resolver = self.node_resolver().await?; let npm_resolver = self.npm_resolver().await?; - let cli_node_resolver = self.cli_node_resolver().await?; let cli_npm_resolver = self.npm_resolver().await?.clone(); let in_npm_pkg_checker = self.in_npm_pkg_checker()?; let maybe_file_watcher_communicator = if cli_options.has_hmr() { @@ -891,6 +927,7 @@ impl CliFactory { let node_code_translator = self.node_code_translator().await?; let cjs_tracker = self.cjs_tracker()?.clone(); let pkg_json_resolver = self.pkg_json_resolver().clone(); + let npm_req_resolver = self.npm_req_resolver().await?; Ok(CliMainWorkerFactory::new( self.blob_store().clone(), @@ -918,7 +955,8 @@ impl CliFactory { self.main_module_graph_container().await?.clone(), self.module_load_preparer().await?.clone(), node_code_translator.clone(), - cli_node_resolver.clone(), + node_resolver.clone(), + npm_req_resolver.clone(), cli_npm_resolver.clone(), NpmModuleLoader::new( self.cjs_tracker()?.clone(), diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 9f26de70cb..044b1573b9 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -344,9 +344,8 @@ impl<'a> TsResponseImportMapper<'a> { { let in_npm_pkg = self .resolver - .maybe_node_resolver(Some(&self.file_referrer)) - .map(|n| n.in_npm_package(specifier)) - .unwrap_or(false); + .in_npm_pkg_checker(Some(&self.file_referrer)) + .in_npm_package(specifier); if in_npm_pkg { if let Ok(Some(pkg_id)) = npm_resolver.resolve_pkg_id_from_specifier(specifier) diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 37f63b912c..399b896381 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -15,6 +15,9 @@ use deno_graph::Range; use deno_npm::NpmSystemInfo; use deno_path_util::url_from_directory_path; use deno_path_util::url_to_file_path; +use deno_resolver::npm::NpmReqResolverOptions; +use deno_resolver::DenoResolverOptions; +use deno_resolver::NodeAndNpmReqResolver; use deno_runtime::deno_fs; use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_node::PackageJson; @@ -43,6 +46,7 @@ use crate::args::CacheSetting; use crate::args::CliLockfile; use crate::args::NpmInstallDepsProvider; use crate::cache::DenoCacheEnvFsAdapter; +use crate::factory::Deferred; use crate::graph_util::CliJsrUrlProvider; use crate::http_util::HttpClientProvider; use crate::lsp::config::Config; @@ -57,8 +61,9 @@ use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; use crate::npm::CreateInNpmPkgCheckerOptions; use crate::npm::ManagedCliNpmResolver; +use crate::resolver::CliDenoResolver; use crate::resolver::CliDenoResolverFs; -use crate::resolver::CliNodeResolver; +use crate::resolver::CliNpmReqResolver; use crate::resolver::CliResolver; use crate::resolver::CliResolverOptions; use crate::resolver::IsCjsResolver; @@ -71,10 +76,12 @@ use crate::util::progress_bar::ProgressBarStyle; #[derive(Debug, Clone)] struct LspScopeResolver { resolver: Arc, + in_npm_pkg_checker: Arc, jsr_resolver: Option>, npm_resolver: Option>, - node_resolver: Option>, - pkg_json_resolver: Option>, + node_resolver: Option>, + npm_pkg_req_resolver: Option>, + pkg_json_resolver: Arc, redirect_resolver: Option>, graph_imports: Arc>, package_json_deps_by_resolution: Arc>, @@ -83,12 +90,15 @@ struct LspScopeResolver { impl Default for LspScopeResolver { fn default() -> Self { + let factory = ResolverFactory::new(None); Self { - resolver: create_cli_resolver(None, None, None), + resolver: factory.cli_resolver().clone(), + in_npm_pkg_checker: factory.in_npm_pkg_checker().clone(), jsr_resolver: None, npm_resolver: None, node_resolver: None, - pkg_json_resolver: None, + npm_pkg_req_resolver: None, + pkg_json_resolver: factory.pkg_json_resolver().clone(), redirect_resolver: None, graph_imports: Default::default(), package_json_deps_by_resolution: Default::default(), @@ -103,35 +113,16 @@ impl LspScopeResolver { cache: &LspCache, http_client_provider: Option<&Arc>, ) -> Self { - let mut npm_resolver = None; - let mut node_resolver = None; - let fs = Arc::new(deno_fs::RealFs); - let pkg_json_resolver = Arc::new(PackageJsonResolver::new( - deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), - )); - if let Some(http_client) = http_client_provider { - npm_resolver = create_npm_resolver( - config_data.map(|d| d.as_ref()), - cache, - http_client, - &pkg_json_resolver, - ) - .await; - if let Some(npm_resolver) = &npm_resolver { - let in_npm_pkg_checker = create_in_npm_pkg_checker(npm_resolver); - node_resolver = Some(create_node_resolver( - fs.clone(), - in_npm_pkg_checker, - npm_resolver, - pkg_json_resolver.clone(), - )); - } + let mut factory = ResolverFactory::new(config_data); + if let Some(http_client_provider) = http_client_provider { + factory.init_npm_resolver(http_client_provider, cache).await; } - let cli_resolver = create_cli_resolver( - config_data.map(|d| d.as_ref()), - npm_resolver.as_ref(), - node_resolver.as_ref(), - ); + let in_npm_pkg_checker = factory.in_npm_pkg_checker().clone(); + let npm_resolver = factory.npm_resolver().cloned(); + let node_resolver = factory.node_resolver().cloned(); + let npm_pkg_req_resolver = factory.npm_pkg_req_resolver().cloned(); + let cli_resolver = factory.cli_resolver().clone(); + let pkg_json_resolver = factory.pkg_json_resolver().clone(); let jsr_resolver = Some(Arc::new(JsrCacheResolver::new( cache.for_specifier(config_data.map(|d| d.scope.as_ref())), config_data.map(|d| d.as_ref()), @@ -171,7 +162,7 @@ impl LspScopeResolver { }) .unwrap_or_default(); let package_json_deps_by_resolution = (|| { - let node_resolver = node_resolver.as_ref()?; + let npm_pkg_req_resolver = npm_pkg_req_resolver.as_ref()?; let package_json = config_data?.maybe_pkg_json()?; let referrer = package_json.specifier(); let dependencies = package_json.dependencies.as_ref()?; @@ -181,7 +172,7 @@ impl LspScopeResolver { let req_ref = NpmPackageReqReference::from_str(&format!("npm:{name}")).ok()?; let specifier = into_specifier_and_media_type(Some( - node_resolver + npm_pkg_req_resolver .resolve_req_reference( &req_ref, &referrer, @@ -201,10 +192,12 @@ impl LspScopeResolver { Arc::new(package_json_deps_by_resolution.unwrap_or_default()); Self { resolver: cli_resolver, + in_npm_pkg_checker, jsr_resolver, + npm_pkg_req_resolver, npm_resolver, node_resolver, - pkg_json_resolver: Some(pkg_json_resolver), + pkg_json_resolver, redirect_resolver, graph_imports, package_json_deps_by_resolution, @@ -213,34 +206,21 @@ impl LspScopeResolver { } fn snapshot(&self) -> Arc { + let mut factory = ResolverFactory::new(self.config_data.as_ref()); let npm_resolver = self.npm_resolver.as_ref().map(|r| r.clone_snapshotted()); - let fs = Arc::new(deno_fs::RealFs); - let pkg_json_resolver = Arc::new(PackageJsonResolver::new( - deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), - )); - let mut node_resolver = None; if let Some(npm_resolver) = &npm_resolver { - let in_npm_pkg_checker = create_in_npm_pkg_checker(npm_resolver); - node_resolver = Some(create_node_resolver( - fs, - in_npm_pkg_checker, - npm_resolver, - pkg_json_resolver.clone(), - )); + factory.set_npm_resolver(npm_resolver.clone()); } - let graph_resolver = create_cli_resolver( - self.config_data.as_deref(), - npm_resolver.as_ref(), - node_resolver.as_ref(), - ); Arc::new(Self { - resolver: graph_resolver, + resolver: factory.cli_resolver().clone(), + in_npm_pkg_checker: factory.in_npm_pkg_checker().clone(), jsr_resolver: self.jsr_resolver.clone(), - npm_resolver, - node_resolver, + npm_pkg_req_resolver: factory.npm_pkg_req_resolver().cloned(), + npm_resolver: factory.npm_resolver().cloned(), + node_resolver: factory.node_resolver().cloned(), redirect_resolver: self.redirect_resolver.clone(), - pkg_json_resolver: Some(pkg_json_resolver), + pkg_json_resolver: factory.pkg_json_resolver().clone(), graph_imports: self.graph_imports.clone(), package_json_deps_by_resolution: self .package_json_deps_by_resolution @@ -354,12 +334,12 @@ impl LspResolver { resolver.config_data.as_ref() } - pub fn maybe_node_resolver( + pub fn in_npm_pkg_checker( &self, file_referrer: Option<&ModuleSpecifier>, - ) -> Option<&Arc> { + ) -> &Arc { let resolver = self.get_scope_resolver(file_referrer); - resolver.node_resolver.as_ref() + &resolver.in_npm_pkg_checker } pub fn maybe_managed_npm_resolver( @@ -429,9 +409,9 @@ impl LspResolver { file_referrer: Option<&ModuleSpecifier>, ) -> Option<(ModuleSpecifier, MediaType)> { let resolver = self.get_scope_resolver(file_referrer); - let node_resolver = resolver.node_resolver.as_ref()?; + let npm_pkg_req_resolver = resolver.npm_pkg_req_resolver.as_ref()?; Some(into_specifier_and_media_type(Some( - node_resolver + npm_pkg_req_resolver .resolve_req_reference( req_ref, referrer, @@ -483,10 +463,11 @@ impl LspResolver { referrer_kind: NodeModuleKind, ) -> bool { let resolver = self.get_scope_resolver(Some(referrer)); - let Some(node_resolver) = resolver.node_resolver.as_ref() else { + let Some(npm_pkg_req_resolver) = resolver.npm_pkg_req_resolver.as_ref() + else { return false; }; - node_resolver + npm_pkg_req_resolver .resolve_if_for_npm_pkg( specifier_text, referrer, @@ -503,10 +484,9 @@ impl LspResolver { referrer: &ModuleSpecifier, ) -> Result>, ClosestPkgJsonError> { let resolver = self.get_scope_resolver(Some(referrer)); - let Some(pkg_json_resolver) = resolver.pkg_json_resolver.as_ref() else { - return Ok(None); - }; - pkg_json_resolver.get_closest_package_json(referrer) + resolver + .pkg_json_resolver + .get_closest_package_json(referrer) } pub fn resolve_redirects( @@ -558,131 +538,206 @@ impl LspResolver { } } -async fn create_npm_resolver( - config_data: Option<&ConfigData>, - cache: &LspCache, - http_client_provider: &Arc, - pkg_json_resolver: &Arc, -) -> Option> { - let enable_byonm = config_data.map(|d| d.byonm).unwrap_or(false); - let options = if enable_byonm { - CliNpmResolverCreateOptions::Byonm(CliByonmNpmResolverCreateOptions { - fs: CliDenoResolverFs(Arc::new(deno_fs::RealFs)), - pkg_json_resolver: pkg_json_resolver.clone(), - root_node_modules_dir: config_data.and_then(|config_data| { - config_data.node_modules_dir.clone().or_else(|| { - url_to_file_path(&config_data.scope) - .ok() - .map(|p| p.join("node_modules/")) - }) - }), - }) - } else { - let npmrc = config_data - .and_then(|d| d.npmrc.clone()) - .unwrap_or_else(create_default_npmrc); - let npm_cache_dir = Arc::new(NpmCacheDir::new( - &DenoCacheEnvFsAdapter(&deno_fs::RealFs), - cache.deno_dir().npm_folder_path(), - npmrc.get_all_known_registries_urls(), - )); - CliNpmResolverCreateOptions::Managed(CliManagedNpmResolverCreateOptions { - http_client_provider: http_client_provider.clone(), - snapshot: match config_data.and_then(|d| d.lockfile.as_ref()) { - Some(lockfile) => { - CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( - lockfile.clone(), - ) - } - None => CliNpmResolverManagedSnapshotOption::Specified(None), - }, - // Don't provide the lockfile. We don't want these resolvers - // updating it. Only the cache request should update the lockfile. - maybe_lockfile: None, - fs: Arc::new(deno_fs::RealFs), - npm_cache_dir, - // Use an "only" cache setting in order to make the - // user do an explicit "cache" command and prevent - // the cache from being filled with lots of packages while - // the user is typing. - cache_setting: CacheSetting::Only, - text_only_progress_bar: ProgressBar::new(ProgressBarStyle::TextOnly), - maybe_node_modules_path: config_data - .and_then(|d| d.node_modules_dir.clone()), - // only used for top level install, so we can ignore this - npm_install_deps_provider: Arc::new(NpmInstallDepsProvider::empty()), - npmrc, - npm_system_info: NpmSystemInfo::default(), - lifecycle_scripts: Default::default(), - }) - }; - Some(create_cli_npm_resolver_for_lsp(options).await) +#[derive(Default)] +struct ResolverFactoryServices { + cli_resolver: Deferred>, + in_npm_pkg_checker: Deferred>, + node_resolver: Deferred>>, + npm_pkg_req_resolver: Deferred>>, + npm_resolver: Option>, } -fn create_in_npm_pkg_checker( - npm_resolver: &Arc, -) -> Arc { - crate::npm::create_in_npm_pkg_checker(match npm_resolver.as_inner() { - crate::npm::InnerCliNpmResolverRef::Byonm(_) => { - CreateInNpmPkgCheckerOptions::Byonm +struct ResolverFactory<'a> { + config_data: Option<&'a Arc>, + fs: Arc, + pkg_json_resolver: Arc, + services: ResolverFactoryServices, +} + +impl<'a> ResolverFactory<'a> { + pub fn new(config_data: Option<&'a Arc>) -> Self { + let fs = Arc::new(deno_fs::RealFs); + let pkg_json_resolver = Arc::new(PackageJsonResolver::new( + deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), + )); + Self { + config_data, + fs, + pkg_json_resolver, + services: Default::default(), } - crate::npm::InnerCliNpmResolverRef::Managed(m) => { - CreateInNpmPkgCheckerOptions::Managed( - CliManagedInNpmPkgCheckerCreateOptions { - root_cache_dir_url: m.global_cache_root_url(), - maybe_node_modules_path: m.maybe_node_modules_path(), + } + + async fn init_npm_resolver( + &mut self, + http_client_provider: &Arc, + cache: &LspCache, + ) { + let enable_byonm = self.config_data.map(|d| d.byonm).unwrap_or(false); + let options = if enable_byonm { + CliNpmResolverCreateOptions::Byonm(CliByonmNpmResolverCreateOptions { + fs: CliDenoResolverFs(Arc::new(deno_fs::RealFs)), + 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(|| { + url_to_file_path(&config_data.scope) + .ok() + .map(|p| p.join("node_modules/")) + }) + }), + }) + } else { + let npmrc = self + .config_data + .and_then(|d| d.npmrc.clone()) + .unwrap_or_else(create_default_npmrc); + let npm_cache_dir = Arc::new(NpmCacheDir::new( + &DenoCacheEnvFsAdapter(self.fs.as_ref()), + cache.deno_dir().npm_folder_path(), + npmrc.get_all_known_registries_urls(), + )); + CliNpmResolverCreateOptions::Managed(CliManagedNpmResolverCreateOptions { + http_client_provider: http_client_provider.clone(), + snapshot: match self.config_data.and_then(|d| d.lockfile.as_ref()) { + Some(lockfile) => { + CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( + lockfile.clone(), + ) + } + None => CliNpmResolverManagedSnapshotOption::Specified(None), + }, + // Don't provide the lockfile. We don't want these resolvers + // updating it. Only the cache request should update the lockfile. + maybe_lockfile: None, + fs: Arc::new(deno_fs::RealFs), + npm_cache_dir, + // Use an "only" cache setting in order to make the + // user do an explicit "cache" command and prevent + // the cache from being filled with lots of packages while + // the user is typing. + cache_setting: CacheSetting::Only, + text_only_progress_bar: ProgressBar::new(ProgressBarStyle::TextOnly), + maybe_node_modules_path: self + .config_data + .and_then(|d| d.node_modules_dir.clone()), + // only used for top level install, so we can ignore this + npm_install_deps_provider: Arc::new(NpmInstallDepsProvider::empty()), + npmrc, + npm_system_info: NpmSystemInfo::default(), + lifecycle_scripts: Default::default(), + }) + }; + self.set_npm_resolver(create_cli_npm_resolver_for_lsp(options).await); + } + + pub fn set_npm_resolver(&mut self, npm_resolver: Arc) { + self.services.npm_resolver = Some(npm_resolver); + } + + pub fn npm_resolver(&self) -> Option<&Arc> { + self.services.npm_resolver.as_ref() + } + + pub fn cli_resolver(&self) -> &Arc { + self.services.cli_resolver.get_or_init(|| { + let npm_req_resolver = self.npm_pkg_req_resolver().cloned(); + let deno_resolver = Arc::new(CliDenoResolver::new(DenoResolverOptions { + in_npm_pkg_checker: self.in_npm_pkg_checker().clone(), + node_and_req_resolver: match (self.node_resolver(), npm_req_resolver) { + (Some(node_resolver), Some(npm_req_resolver)) => { + Some(NodeAndNpmReqResolver { + node_resolver: node_resolver.clone(), + npm_req_resolver, + }) + } + _ => None, + }, + sloppy_imports_resolver: self + .config_data + .and_then(|d| d.sloppy_imports_resolver.clone()), + workspace_resolver: self + .config_data + .map(|d| d.resolver.clone()) + .unwrap_or_else(|| { + Arc::new(WorkspaceResolver::new_raw( + // this is fine because this is only used before initialization + Arc::new(ModuleSpecifier::parse("file:///").unwrap()), + None, + Vec::new(), + Vec::new(), + PackageJsonDepResolution::Disabled, + )) + }), + is_byonm: self.config_data.map(|d| d.byonm).unwrap_or(false), + maybe_vendor_dir: self.config_data.and_then(|d| d.vendor_dir.as_ref()), + })); + Arc::new(CliResolver::new(CliResolverOptions { + deno_resolver, + npm_resolver: self.npm_resolver().cloned(), + bare_node_builtins_enabled: self + .config_data + .is_some_and(|d| d.unstable.contains("bare-node-builtins")), + })) + }) + } + + pub fn pkg_json_resolver(&self) -> &Arc { + &self.pkg_json_resolver + } + + pub fn in_npm_pkg_checker(&self) -> &Arc { + self.services.in_npm_pkg_checker.get_or_init(|| { + crate::npm::create_in_npm_pkg_checker( + match self.services.npm_resolver.as_ref().map(|r| r.as_inner()) { + Some(crate::npm::InnerCliNpmResolverRef::Byonm(_)) | None => { + CreateInNpmPkgCheckerOptions::Byonm + } + Some(crate::npm::InnerCliNpmResolverRef::Managed(m)) => { + CreateInNpmPkgCheckerOptions::Managed( + CliManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: m.global_cache_root_url(), + maybe_node_modules_path: m.maybe_node_modules_path(), + }, + ) + } }, ) - } - }) -} + }) + } -fn create_node_resolver( - fs: Arc, - in_npm_pkg_checker: Arc, - npm_resolver: &Arc, - pkg_json_resolver: Arc, -) -> Arc { - let node_resolver_inner = Arc::new(NodeResolver::new( - deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), - in_npm_pkg_checker.clone(), - npm_resolver.clone().into_npm_resolver(), - pkg_json_resolver.clone(), - )); - Arc::new(CliNodeResolver::new( - fs, - in_npm_pkg_checker, - node_resolver_inner, - npm_resolver.clone(), - )) -} + pub fn node_resolver(&self) -> Option<&Arc> { + self + .services + .node_resolver + .get_or_init(|| { + let npm_resolver = self.services.npm_resolver.as_ref()?; + Some(Arc::new(NodeResolver::new( + deno_runtime::deno_node::DenoFsNodeResolverEnv::new(self.fs.clone()), + self.in_npm_pkg_checker().clone(), + npm_resolver.clone().into_npm_pkg_folder_resolver(), + self.pkg_json_resolver.clone(), + ))) + }) + .as_ref() + } -fn create_cli_resolver( - config_data: Option<&ConfigData>, - npm_resolver: Option<&Arc>, - node_resolver: Option<&Arc>, -) -> Arc { - Arc::new(CliResolver::new(CliResolverOptions { - node_resolver: node_resolver.cloned(), - npm_resolver: npm_resolver.cloned(), - workspace_resolver: config_data.map(|d| d.resolver.clone()).unwrap_or_else( - || { - Arc::new(WorkspaceResolver::new_raw( - // this is fine because this is only used before initialization - Arc::new(ModuleSpecifier::parse("file:///").unwrap()), - None, - Vec::new(), - Vec::new(), - PackageJsonDepResolution::Disabled, - )) - }, - ), - maybe_vendor_dir: config_data.and_then(|d| d.vendor_dir.as_ref()), - bare_node_builtins_enabled: config_data - .is_some_and(|d| d.unstable.contains("bare-node-builtins")), - sloppy_imports_resolver: config_data - .and_then(|d| d.sloppy_imports_resolver.clone()), - })) + pub fn npm_pkg_req_resolver(&self) -> Option<&Arc> { + self + .services + .npm_pkg_req_resolver + .get_or_init(|| { + let node_resolver = self.node_resolver()?; + let npm_resolver = self.npm_resolver()?; + Some(Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { + byonm_resolver: (npm_resolver.clone()).into_maybe_byonm(), + fs: CliDenoResolverFs(self.fs.clone()), + in_npm_pkg_checker: self.in_npm_pkg_checker().clone(), + node_resolver: node_resolver.clone(), + npm_req_resolver: npm_resolver.clone().into_npm_req_resolver(), + }))) + }) + .as_ref() + } } #[derive(Debug, Eq, PartialEq)] diff --git a/cli/main.rs b/cli/main.rs index 20d2cb6bff..7d3ef0e6a0 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -37,6 +37,7 @@ use crate::util::v8::init_v8_flags; use args::TaskFlags; use deno_resolver::npm::ByonmResolvePkgFolderFromDenoReqError; +use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; use deno_runtime::WorkerExecutionMode; pub use deno_runtime::UNSTABLE_GRANULAR_FLAGS; @@ -50,7 +51,6 @@ use deno_runtime::fmt_errors::format_js_error; use deno_runtime::tokio_util::create_and_run_current_thread_with_maybe_metrics; use deno_terminal::colors; use factory::CliFactory; -use npm::ResolvePkgFolderFromDenoReqError; use standalone::MODULE_NOT_FOUND; use standalone::UNSUPPORTED_SCHEME; use std::env; diff --git a/cli/module_loader.rs b/cli/module_loader.rs index f9c974d77e..b9adfe642c 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -27,7 +27,7 @@ use crate::node; use crate::node::CliNodeCodeTranslator; use crate::npm::CliNpmResolver; use crate::resolver::CjsTracker; -use crate::resolver::CliNodeResolver; +use crate::resolver::CliNpmReqResolver; use crate::resolver::CliResolver; use crate::resolver::ModuleCodeStringSource; use crate::resolver::NotSupportedKindInNpmError; @@ -70,6 +70,7 @@ use deno_runtime::code_cache; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::create_host_defined_options; use deno_runtime::deno_node::NodeRequireLoader; +use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::npm::NpmPackageReqReference; use node_resolver::errors::ClosestPkgJsonError; @@ -215,7 +216,8 @@ struct SharedCliModuleLoaderState { main_module_graph_container: Arc, module_load_preparer: Arc, node_code_translator: Arc, - node_resolver: Arc, + node_resolver: Arc, + npm_req_resolver: Arc, npm_resolver: Arc, npm_module_loader: NpmModuleLoader, parsed_source_cache: Arc, @@ -238,7 +240,8 @@ impl CliModuleLoaderFactory { main_module_graph_container: Arc, module_load_preparer: Arc, node_code_translator: Arc, - node_resolver: Arc, + node_resolver: Arc, + npm_req_resolver: Arc, npm_resolver: Arc, npm_module_loader: NpmModuleLoader, parsed_source_cache: Arc, @@ -264,6 +267,7 @@ impl CliModuleLoaderFactory { module_load_preparer, node_code_translator, node_resolver, + npm_req_resolver, npm_resolver, npm_module_loader, parsed_source_cache, @@ -425,7 +429,7 @@ impl if let Some(code_source) = self.load_prepared_module(specifier).await? { return Ok(code_source); } - if self.shared.node_resolver.in_npm_package(specifier) { + if self.shared.in_npm_pkg_checker.in_npm_package(specifier) { return self .shared .npm_module_loader @@ -470,7 +474,7 @@ impl raw_specifier: &str, referrer: &ModuleSpecifier, ) -> Result { - if self.shared.node_resolver.in_npm_package(referrer) { + if self.shared.in_npm_pkg_checker.in_npm_package(referrer) { return Ok( self .shared @@ -518,12 +522,16 @@ impl if self.shared.is_repl { if let Ok(reference) = NpmPackageReqReference::from_specifier(&specifier) { - return self.shared.node_resolver.resolve_req_reference( - &reference, - referrer, - self.shared.cjs_tracker.get_referrer_kind(referrer), - NodeResolutionMode::Execution, - ); + return self + .shared + .npm_req_resolver + .resolve_req_reference( + &reference, + referrer, + self.shared.cjs_tracker.get_referrer_kind(referrer), + NodeResolutionMode::Execution, + ) + .map_err(AnyError::from); } } @@ -538,7 +546,7 @@ impl self .shared .node_resolver - .resolve_package_sub_path_from_deno_module( + .resolve_package_subpath_from_deno_module( &package_folder, module.nv_reference.sub_path(), Some(referrer), @@ -828,7 +836,7 @@ impl ModuleLoader name: &str, ) -> Option> { let name = deno_core::ModuleSpecifier::parse(name).ok()?; - if self.0.shared.node_resolver.in_npm_package(&name) { + if self.0.shared.in_npm_pkg_checker.in_npm_package(&name) { Some(create_host_defined_options(scope)) } else { None @@ -865,7 +873,7 @@ impl ModuleLoader _maybe_referrer: Option, is_dynamic: bool, ) -> Pin>>> { - if self.0.shared.node_resolver.in_npm_package(specifier) { + if self.0.shared.in_npm_pkg_checker.in_npm_package(specifier) { return Box::pin(deno_core::futures::future::ready(Ok(()))); } diff --git a/cli/node.rs b/cli/node.rs index 8235745a91..bc39cdbde9 100644 --- a/cli/node.rs +++ b/cli/node.rs @@ -7,8 +7,6 @@ use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_graph::ParsedSourceStore; -use deno_path_util::url_from_file_path; -use deno_path_util::url_to_file_path; use deno_runtime::deno_fs; use deno_runtime::deno_node::DenoFsNodeResolverEnv; use node_resolver::analyze::CjsAnalysis as ExtNodeCjsAnalysis; @@ -22,7 +20,6 @@ use crate::cache::CacheDBHash; use crate::cache::NodeAnalysisCache; use crate::cache::ParsedSourceCache; use crate::resolver::CjsTracker; -use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs; pub type CliNodeCodeTranslator = NodeCodeTranslator; @@ -37,13 +34,9 @@ pub fn resolve_specifier_into_node_modules( specifier: &ModuleSpecifier, fs: &dyn deno_fs::FileSystem, ) -> ModuleSpecifier { - url_to_file_path(specifier) - .ok() - // this path might not exist at the time the graph is being created - // because the node_modules folder might not yet exist - .and_then(|path| canonicalize_path_maybe_not_exists_with_fs(&path, fs).ok()) - .and_then(|path| url_from_file_path(&path).ok()) - .unwrap_or_else(|| specifier.clone()) + node_resolver::resolve_specifier_into_node_modules(specifier, &|path| { + fs.realpath_sync(path).map_err(|err| err.into_io_error()) + }) } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/cli/npm/byonm.rs b/cli/npm/byonm.rs index 45fa4cfd1f..eca399251b 100644 --- a/cli/npm/byonm.rs +++ b/cli/npm/byonm.rs @@ -2,19 +2,17 @@ use std::borrow::Cow; use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; use deno_core::error::AnyError; use deno_core::serde_json; -use deno_core::url::Url; use deno_resolver::npm::ByonmNpmResolver; use deno_resolver::npm::ByonmNpmResolverCreateOptions; +use deno_resolver::npm::CliNpmReqResolver; use deno_runtime::deno_node::DenoFsNodeResolverEnv; use deno_runtime::deno_node::NodePermissions; use deno_runtime::ops::process::NpmProcessStateProvider; -use deno_semver::package::PackageReq; -use node_resolver::NpmResolver; +use node_resolver::NpmPackageFolderResolver; use crate::args::NpmProcessState; use crate::args::NpmProcessStateKind; @@ -22,7 +20,6 @@ use crate::resolver::CliDenoResolverFs; use super::CliNpmResolver; use super::InnerCliNpmResolverRef; -use super::ResolvePkgFolderFromDenoReqError; pub type CliByonmNpmResolverCreateOptions = ByonmNpmResolverCreateOptions; @@ -47,7 +44,13 @@ impl NpmProcessStateProvider for CliByonmWrapper { } impl CliNpmResolver for CliByonmNpmResolver { - fn into_npm_resolver(self: Arc) -> Arc { + fn into_npm_pkg_folder_resolver( + self: Arc, + ) -> Arc { + self + } + + fn into_npm_req_resolver(self: Arc) -> Arc { self } @@ -57,6 +60,10 @@ impl CliNpmResolver for CliByonmNpmResolver { Arc::new(CliByonmWrapper(self)) } + fn into_maybe_byonm(self: Arc) -> Option> { + Some(self) + } + fn clone_snapshotted(&self) -> Arc { Arc::new(self.clone()) } @@ -69,17 +76,6 @@ impl CliNpmResolver for CliByonmNpmResolver { self.root_node_modules_dir() } - fn resolve_pkg_folder_from_deno_module_req( - &self, - req: &PackageReq, - referrer: &Url, - ) -> Result { - ByonmNpmResolver::resolve_pkg_folder_from_deno_module_req( - self, req, referrer, - ) - .map_err(ResolvePkgFolderFromDenoReqError::Byonm) - } - fn ensure_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs index 4a91bc3474..2e64f5f188 100644 --- a/cli/npm/managed/mod.rs +++ b/cli/npm/managed/mod.rs @@ -22,6 +22,7 @@ use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; +use deno_resolver::npm::CliNpmReqResolver; use deno_runtime::colors; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::NodePermissions; @@ -31,7 +32,7 @@ use deno_semver::package::PackageReq; use node_resolver::errors::PackageFolderResolveError; use node_resolver::errors::PackageFolderResolveIoError; use node_resolver::InNpmPackageChecker; -use node_resolver::NpmResolver; +use node_resolver::NpmPackageFolderResolver; use resolution::AddPkgReqsResult; use crate::args::CliLockfile; @@ -605,7 +606,7 @@ fn npm_process_state( .unwrap() } -impl NpmResolver for ManagedCliNpmResolver { +impl NpmPackageFolderResolver for ManagedCliNpmResolver { fn resolve_package_folder_from_package( &self, name: &str, @@ -635,8 +636,29 @@ impl NpmProcessStateProvider for ManagedCliNpmResolver { } } +impl CliNpmReqResolver for ManagedCliNpmResolver { + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + _referrer: &ModuleSpecifier, + ) -> Result { + let pkg_id = self + .resolve_pkg_id_from_pkg_req(req) + .map_err(|err| ResolvePkgFolderFromDenoReqError::Managed(err.into()))?; + self + .resolve_pkg_folder_from_pkg_id(&pkg_id) + .map_err(ResolvePkgFolderFromDenoReqError::Managed) + } +} + impl CliNpmResolver for ManagedCliNpmResolver { - fn into_npm_resolver(self: Arc) -> Arc { + fn into_npm_pkg_folder_resolver( + self: Arc, + ) -> Arc { + self + } + + fn into_npm_req_resolver(self: Arc) -> Arc { self } @@ -687,19 +709,6 @@ impl CliNpmResolver for ManagedCliNpmResolver { self.fs_resolver.node_modules_path() } - fn resolve_pkg_folder_from_deno_module_req( - &self, - req: &PackageReq, - _referrer: &ModuleSpecifier, - ) -> Result { - let pkg_id = self - .resolve_pkg_id_from_pkg_req(req) - .map_err(|err| ResolvePkgFolderFromDenoReqError::Managed(err.into()))?; - self - .resolve_pkg_folder_from_pkg_id(&pkg_id) - .map_err(ResolvePkgFolderFromDenoReqError::Managed) - } - fn ensure_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index 0d434ca27f..0e955ac5b4 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -6,19 +6,18 @@ mod managed; use std::borrow::Cow; use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; use common::maybe_auth_header_for_npm_registry; use dashmap::DashMap; -use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::serde_json; use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::registry::NpmPackageInfo; use deno_resolver::npm::ByonmInNpmPackageChecker; use deno_resolver::npm::ByonmNpmResolver; -use deno_resolver::npm::ByonmResolvePkgFolderFromDenoReqError; +use deno_resolver::npm::CliNpmReqResolver; +use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; use deno_runtime::deno_node::NodePermissions; use deno_runtime::ops::process::NpmProcessStateProvider; use deno_semver::package::PackageNv; @@ -26,8 +25,7 @@ use deno_semver::package::PackageReq; use managed::cache::registry_info::get_package_url; use managed::create_managed_in_npm_pkg_checker; use node_resolver::InNpmPackageChecker; -use node_resolver::NpmResolver; -use thiserror::Error; +use node_resolver::NpmPackageFolderResolver; use crate::file_fetcher::FileFetcher; @@ -38,14 +36,6 @@ pub use self::managed::CliManagedNpmResolverCreateOptions; pub use self::managed::CliNpmResolverManagedSnapshotOption; pub use self::managed::ManagedCliNpmResolver; -#[derive(Debug, Error)] -pub enum ResolvePkgFolderFromDenoReqError { - #[error(transparent)] - Managed(deno_core::error::AnyError), - #[error(transparent)] - Byonm(#[from] ByonmResolvePkgFolderFromDenoReqError), -} - pub enum CliNpmResolverCreateOptions { Managed(CliManagedNpmResolverCreateOptions), Byonm(CliByonmNpmResolverCreateOptions), @@ -95,11 +85,17 @@ pub enum InnerCliNpmResolverRef<'a> { Byonm(&'a CliByonmNpmResolver), } -pub trait CliNpmResolver: NpmResolver { - fn into_npm_resolver(self: Arc) -> Arc; +pub trait CliNpmResolver: NpmPackageFolderResolver + CliNpmReqResolver { + fn into_npm_pkg_folder_resolver( + self: Arc, + ) -> Arc; + fn into_npm_req_resolver(self: Arc) -> Arc; fn into_process_state_provider( self: Arc, ) -> Arc; + fn into_maybe_byonm(self: Arc) -> Option> { + None + } fn clone_snapshotted(&self) -> Arc; @@ -121,12 +117,6 @@ pub trait CliNpmResolver: NpmResolver { fn root_node_modules_path(&self) -> Option<&Path>; - fn resolve_pkg_folder_from_deno_module_req( - &self, - req: &PackageReq, - referrer: &ModuleSpecifier, - ) -> Result; - fn ensure_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, diff --git a/cli/resolver.rs b/cli/resolver.rs index 786e5d0dbc..a2dd47430f 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -4,10 +4,8 @@ use async_trait::async_trait; use dashmap::DashMap; use dashmap::DashSet; use deno_ast::MediaType; -use deno_config::workspace::MappedResolution; use deno_config::workspace::MappedResolutionDiagnostic; use deno_config::workspace::MappedResolutionError; -use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; @@ -20,28 +18,14 @@ use deno_graph::source::UnknownBuiltInNodeModuleError; use deno_graph::NpmLoadError; use deno_graph::NpmResolvePkgReqsResult; use deno_npm::resolution::NpmResolutionError; -use deno_package_json::PackageJsonDepValue; -use deno_resolver::sloppy_imports::SloppyImportsResolutionMode; use deno_resolver::sloppy_imports::SloppyImportsResolver; use deno_runtime::colors; use deno_runtime::deno_fs; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::is_builtin_node_module; -use deno_runtime::deno_node::NodeResolver; -use deno_runtime::deno_node::PackageJsonResolver; -use deno_semver::npm::NpmPackageReqReference; +use deno_runtime::deno_node::DenoFsNodeResolverEnv; use deno_semver::package::PackageReq; -use node_resolver::errors::ClosestPkgJsonError; -use node_resolver::errors::NodeResolveError; -use node_resolver::errors::NodeResolveErrorKind; -use node_resolver::errors::PackageFolderResolveErrorKind; -use node_resolver::errors::PackageFolderResolveIoError; -use node_resolver::errors::PackageNotFoundError; -use node_resolver::errors::PackageResolveErrorKind; -use node_resolver::errors::PackageSubpathResolveError; -use node_resolver::InNpmPackageChecker; use node_resolver::NodeModuleKind; -use node_resolver::NodeResolution; use node_resolver::NodeResolutionMode; use std::borrow::Cow; use std::path::Path; @@ -56,6 +40,20 @@ use crate::npm::InnerCliNpmResolverRef; use crate::util::sync::AtomicFlag; use crate::util::text_encoding::from_utf8_lossy_owned; +pub type CjsTracker = deno_resolver::cjs::CjsTracker; +pub type IsCjsResolver = + deno_resolver::cjs::IsCjsResolver; +pub type IsCjsResolverOptions = deno_resolver::cjs::IsCjsResolverOptions; +pub type CliSloppyImportsResolver = + SloppyImportsResolver; +pub type CliDenoResolver = deno_resolver::DenoResolver< + CliDenoResolverFs, + DenoFsNodeResolverEnv, + SloppyImportsCachedFs, +>; +pub type CliNpmReqResolver = + deno_resolver::npm::NpmReqResolver; + pub struct ModuleCodeStringSource { pub code: ModuleSourceCode, pub found_url: ModuleSpecifier, @@ -77,6 +75,10 @@ impl deno_resolver::fs::DenoResolverFs for CliDenoResolverFs { self.0.realpath_sync(path).map_err(|e| e.into_io_error()) } + fn exists_sync(&self, path: &Path) -> bool { + self.0.exists_sync(path) + } + fn is_dir_sync(&self, path: &Path) -> bool { self.0.is_dir_sync(path) } @@ -102,211 +104,6 @@ impl deno_resolver::fs::DenoResolverFs for CliDenoResolverFs { } } -#[derive(Debug)] -pub struct CliNodeResolver { - fs: Arc, - in_npm_pkg_checker: Arc, - node_resolver: Arc, - npm_resolver: Arc, -} - -impl CliNodeResolver { - pub fn new( - fs: Arc, - in_npm_pkg_checker: Arc, - node_resolver: Arc, - npm_resolver: Arc, - ) -> Self { - Self { - fs, - in_npm_pkg_checker, - node_resolver, - npm_resolver, - } - } - - pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { - self.in_npm_pkg_checker.in_npm_package(specifier) - } - - pub fn resolve_if_for_npm_pkg( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - mode: NodeResolutionMode, - ) -> Result, AnyError> { - let resolution_result = - self.resolve(specifier, referrer, referrer_kind, mode); - match resolution_result { - Ok(res) => Ok(Some(res)), - Err(err) => { - let err = err.into_kind(); - match err { - NodeResolveErrorKind::RelativeJoin(_) - | NodeResolveErrorKind::PackageImportsResolve(_) - | NodeResolveErrorKind::UnsupportedEsmUrlScheme(_) - | NodeResolveErrorKind::DataUrlReferrer(_) - | NodeResolveErrorKind::TypesNotFound(_) - | NodeResolveErrorKind::FinalizeResolution(_) => Err(err.into()), - NodeResolveErrorKind::PackageResolve(err) => { - let err = err.into_kind(); - match err { - PackageResolveErrorKind::ClosestPkgJson(_) - | PackageResolveErrorKind::InvalidModuleSpecifier(_) - | PackageResolveErrorKind::ExportsResolve(_) - | PackageResolveErrorKind::SubpathResolve(_) => Err(err.into()), - PackageResolveErrorKind::PackageFolderResolve(err) => { - match err.as_kind() { - PackageFolderResolveErrorKind::Io( - PackageFolderResolveIoError { package_name, .. }, - ) - | PackageFolderResolveErrorKind::PackageNotFound( - PackageNotFoundError { package_name, .. }, - ) => { - if self.in_npm_package(referrer) { - return Err(err.into()); - } - if let Some(byonm_npm_resolver) = - self.npm_resolver.as_byonm() - { - if byonm_npm_resolver - .find_ancestor_package_json_with_dep( - package_name, - referrer, - ) - .is_some() - { - return Err(anyhow!( - concat!( - "Could not resolve \"{}\", but found it in a package.json. ", - "Deno expects the node_modules/ directory to be up to date. ", - "Did you forget to run `deno install`?" - ), - specifier - )); - } - } - Ok(None) - } - PackageFolderResolveErrorKind::ReferrerNotFound(_) => { - if self.in_npm_package(referrer) { - return Err(err.into()); - } - Ok(None) - } - } - } - } - } - } - } - } - } - - pub fn resolve( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - mode: NodeResolutionMode, - ) -> Result { - self - .node_resolver - .resolve(specifier, referrer, referrer_kind, mode) - } - - pub fn resolve_req_reference( - &self, - req_ref: &NpmPackageReqReference, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - mode: NodeResolutionMode, - ) -> Result { - self.resolve_req_with_sub_path( - req_ref.req(), - req_ref.sub_path(), - referrer, - referrer_kind, - mode, - ) - } - - pub fn resolve_req_with_sub_path( - &self, - req: &PackageReq, - sub_path: Option<&str>, - referrer: &ModuleSpecifier, - referrer_kind: NodeModuleKind, - mode: NodeResolutionMode, - ) -> Result { - let package_folder = self - .npm_resolver - .resolve_pkg_folder_from_deno_module_req(req, referrer)?; - let resolution_result = self.resolve_package_sub_path_from_deno_module( - &package_folder, - sub_path, - Some(referrer), - referrer_kind, - mode, - ); - match resolution_result { - Ok(url) => Ok(url), - Err(err) => { - if self.npm_resolver.as_byonm().is_some() { - let package_json_path = package_folder.join("package.json"); - if !self.fs.exists_sync(&package_json_path) { - return Err(anyhow!( - "Could not find '{}'. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", - package_json_path.display(), - )); - } - } - Err(err.into()) - } - } - } - - pub fn resolve_package_sub_path_from_deno_module( - &self, - package_folder: &Path, - sub_path: Option<&str>, - maybe_referrer: Option<&ModuleSpecifier>, - referrer_kind: NodeModuleKind, - mode: NodeResolutionMode, - ) -> Result { - self.node_resolver.resolve_package_subpath_from_deno_module( - package_folder, - sub_path, - maybe_referrer, - referrer_kind, - mode, - ) - } - - pub fn handle_if_in_node_modules( - &self, - specifier: &ModuleSpecifier, - ) -> Result, AnyError> { - // skip canonicalizing if we definitely know it's unnecessary - if specifier.scheme() == "file" - && specifier.path().contains("/node_modules/") - { - // Specifiers in the node_modules directory are canonicalized - // so canoncalize then check if it's in the node_modules directory. - // If so, check if we need to store this specifier as being a CJS - // resolution. - let specifier = crate::node::resolve_specifier_into_node_modules( - specifier, - self.fs.as_ref(), - ); - return Ok(Some(specifier)); - } - - Ok(None) - } -} - #[derive(Debug, Error)] #[error("{media_type} files are not supported in npm packages: {specifier}")] pub struct NotSupportedKindInNpmError { @@ -409,305 +206,36 @@ impl NpmModuleLoader { } } -/// Keeps track of what module specifiers were resolved as CJS. -/// -/// Modules that are `.js` or `.ts` are only known to be CJS or -/// ESM after they're loaded based on their contents. So these files -/// will be "maybe CJS" until they're loaded. -#[derive(Debug)] -pub struct CjsTracker { - is_cjs_resolver: IsCjsResolver, - known: DashMap, +pub struct CliResolverOptions { + pub deno_resolver: Arc, + pub npm_resolver: Option>, + pub bare_node_builtins_enabled: bool, } -impl CjsTracker { - pub fn new( - in_npm_pkg_checker: Arc, - pkg_json_resolver: Arc, - options: IsCjsResolverOptions, - ) -> Self { - Self { - is_cjs_resolver: IsCjsResolver::new( - in_npm_pkg_checker, - pkg_json_resolver, - options, - ), - known: Default::default(), - } - } - - /// Checks whether the file might be treated as CJS, but it's not for sure - /// yet because the source hasn't been loaded to see whether it contains - /// imports or exports. - pub fn is_maybe_cjs( - &self, - specifier: &ModuleSpecifier, - media_type: MediaType, - ) -> Result { - self.treat_as_cjs_with_is_script(specifier, media_type, None) - } - - /// Gets whether the file is CJS. If true, this is for sure - /// cjs because `is_script` is provided. - /// - /// `is_script` should be `true` when the contents of the file at the - /// provided specifier are known to be a script and not an ES module. - pub fn is_cjs_with_known_is_script( - &self, - specifier: &ModuleSpecifier, - media_type: MediaType, - is_script: bool, - ) -> Result { - self.treat_as_cjs_with_is_script(specifier, media_type, Some(is_script)) - } - - fn treat_as_cjs_with_is_script( - &self, - specifier: &ModuleSpecifier, - media_type: MediaType, - is_script: Option, - ) -> Result { - let kind = match self - .get_known_kind_with_is_script(specifier, media_type, is_script) - { - Some(kind) => kind, - None => self.is_cjs_resolver.check_based_on_pkg_json(specifier)?, - }; - Ok(kind == NodeModuleKind::Cjs) - } - - pub fn get_known_kind( - &self, - specifier: &ModuleSpecifier, - media_type: MediaType, - ) -> Option { - self.get_known_kind_with_is_script(specifier, media_type, None) - } - - pub fn get_referrer_kind( - &self, - specifier: &ModuleSpecifier, - ) -> NodeModuleKind { - if specifier.scheme() != "file" { - return NodeModuleKind::Esm; - } - self - .get_known_kind(specifier, MediaType::from_specifier(specifier)) - .unwrap_or(NodeModuleKind::Esm) - } - - fn get_known_kind_with_is_script( - &self, - specifier: &ModuleSpecifier, - media_type: MediaType, - is_script: Option, - ) -> Option { - self.is_cjs_resolver.get_known_kind_with_is_script( - specifier, - media_type, - is_script, - &self.known, - ) - } -} - -#[derive(Debug)] -pub struct IsCjsResolverOptions { - pub detect_cjs: bool, - pub is_node_main: bool, -} - -#[derive(Debug)] -pub struct IsCjsResolver { - in_npm_pkg_checker: Arc, - pkg_json_resolver: Arc, - options: IsCjsResolverOptions, -} - -impl IsCjsResolver { - pub fn new( - in_npm_pkg_checker: Arc, - pkg_json_resolver: Arc, - options: IsCjsResolverOptions, - ) -> Self { - Self { - in_npm_pkg_checker, - pkg_json_resolver, - options, - } - } - - pub fn get_lsp_referrer_kind( - &self, - specifier: &ModuleSpecifier, - is_script: Option, - ) -> NodeModuleKind { - if specifier.scheme() != "file" { - return NodeModuleKind::Esm; - } - match MediaType::from_specifier(specifier) { - MediaType::Mts | MediaType::Mjs | MediaType::Dmts => NodeModuleKind::Esm, - MediaType::Cjs | MediaType::Cts | MediaType::Dcts => NodeModuleKind::Cjs, - MediaType::Dts => { - // dts files are always determined based on the package.json because - // they contain imports/exports even when considered CJS - self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm) - } - MediaType::Wasm | - MediaType::Json => NodeModuleKind::Esm, - MediaType::JavaScript - | MediaType::Jsx - | MediaType::TypeScript - | MediaType::Tsx - // treat these as unknown - | MediaType::Css - | MediaType::SourceMap - | MediaType::Unknown => { - match is_script { - Some(true) => self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm), - Some(false) | None => NodeModuleKind::Esm, - } - } - } - } - - fn get_known_kind_with_is_script( - &self, - specifier: &ModuleSpecifier, - media_type: MediaType, - is_script: Option, - known_cache: &DashMap, - ) -> Option { - if specifier.scheme() != "file" { - return Some(NodeModuleKind::Esm); - } - - match media_type { - MediaType::Mts | MediaType::Mjs | MediaType::Dmts => Some(NodeModuleKind::Esm), - MediaType::Cjs | MediaType::Cts | MediaType::Dcts => Some(NodeModuleKind::Cjs), - MediaType::Dts => { - // dts files are always determined based on the package.json because - // they contain imports/exports even when considered CJS - if let Some(value) = known_cache.get(specifier).map(|v| *v) { - Some(value) - } else { - let value = self.check_based_on_pkg_json(specifier).ok(); - if let Some(value) = value { - known_cache.insert(specifier.clone(), value); - } - Some(value.unwrap_or(NodeModuleKind::Esm)) - } - } - MediaType::Wasm | - MediaType::Json => Some(NodeModuleKind::Esm), - MediaType::JavaScript - | MediaType::Jsx - | MediaType::TypeScript - | MediaType::Tsx - // treat these as unknown - | MediaType::Css - | MediaType::SourceMap - | MediaType::Unknown => { - if let Some(value) = known_cache.get(specifier).map(|v| *v) { - if value == NodeModuleKind::Cjs && is_script == Some(false) { - // we now know this is actually esm - known_cache.insert(specifier.clone(), NodeModuleKind::Esm); - Some(NodeModuleKind::Esm) - } else { - Some(value) - } - } else if is_script == Some(false) { - // we know this is esm - known_cache.insert(specifier.clone(), NodeModuleKind::Esm); - Some(NodeModuleKind::Esm) - } else { - None - } - } - } - } - - fn check_based_on_pkg_json( - &self, - specifier: &ModuleSpecifier, - ) -> Result { - if self.in_npm_pkg_checker.in_npm_package(specifier) { - if let Some(pkg_json) = - self.pkg_json_resolver.get_closest_package_json(specifier)? - { - let is_file_location_cjs = pkg_json.typ != "module"; - Ok(if is_file_location_cjs { - NodeModuleKind::Cjs - } else { - NodeModuleKind::Esm - }) - } else { - Ok(NodeModuleKind::Cjs) - } - } else if self.options.detect_cjs || self.options.is_node_main { - if let Some(pkg_json) = - self.pkg_json_resolver.get_closest_package_json(specifier)? - { - let is_cjs_type = pkg_json.typ == "commonjs" - || self.options.is_node_main && pkg_json.typ == "none"; - Ok(if is_cjs_type { - NodeModuleKind::Cjs - } else { - NodeModuleKind::Esm - }) - } else if self.options.is_node_main { - Ok(NodeModuleKind::Cjs) - } else { - Ok(NodeModuleKind::Esm) - } - } else { - Ok(NodeModuleKind::Esm) - } - } -} - -pub type CliSloppyImportsResolver = - SloppyImportsResolver; - /// A resolver that takes care of resolution, taking into account loaded /// import map, JSX settings. #[derive(Debug)] pub struct CliResolver { - node_resolver: Option>, + deno_resolver: Arc, npm_resolver: Option>, - sloppy_imports_resolver: Option>, - workspace_resolver: Arc, - maybe_vendor_specifier: Option, found_package_json_dep_flag: AtomicFlag, bare_node_builtins_enabled: bool, warned_pkgs: DashSet, } -pub struct CliResolverOptions<'a> { - pub node_resolver: Option>, - pub npm_resolver: Option>, - pub sloppy_imports_resolver: Option>, - pub workspace_resolver: Arc, - pub bare_node_builtins_enabled: bool, - pub maybe_vendor_dir: Option<&'a PathBuf>, -} - impl CliResolver { pub fn new(options: CliResolverOptions) -> Self { Self { - node_resolver: options.node_resolver, + deno_resolver: options.deno_resolver, npm_resolver: options.npm_resolver, - sloppy_imports_resolver: options.sloppy_imports_resolver, - workspace_resolver: options.workspace_resolver, - maybe_vendor_specifier: options - .maybe_vendor_dir - .and_then(|v| ModuleSpecifier::from_directory_path(v).ok()), found_package_json_dep_flag: Default::default(), bare_node_builtins_enabled: options.bare_node_builtins_enabled, warned_pkgs: Default::default(), } } + // todo(dsherret): move this off CliResolver as CliResolver is acting + // like a factory by doing this (it's beyond its responsibility) pub fn create_graph_npm_resolver(&self) -> WorkerCliNpmGraphResolver { WorkerCliNpmGraphResolver { npm_resolver: self.npm_resolver.as_ref(), @@ -730,223 +258,50 @@ impl CliResolver { } } - let referrer = &referrer_range.specifier; + let resolution = self + .deno_resolver + .resolve( + raw_specifier, + &referrer_range.specifier, + referrer_kind, + to_node_mode(mode), + ) + .map_err(|err| match err.into_kind() { + deno_resolver::DenoResolveErrorKind::MappedResolution( + mapped_resolution_error, + ) => match mapped_resolution_error { + MappedResolutionError::Specifier(e) => ResolveError::Specifier(e), + // deno_graph checks specifically for an ImportMapError + MappedResolutionError::ImportMap(e) => ResolveError::Other(e.into()), + err => ResolveError::Other(err.into()), + }, + err => ResolveError::Other(err.into()), + })?; - // Use node resolution if we're in an npm package - if let Some(node_resolver) = self.node_resolver.as_ref() { - if referrer.scheme() == "file" && node_resolver.in_npm_package(referrer) { - return node_resolver - .resolve(raw_specifier, referrer, referrer_kind, to_node_mode(mode)) - .map(|res| res.into_url()) - .map_err(|e| ResolveError::Other(e.into())); - } + if resolution.found_package_json_dep { + // mark that we need to do an "npm install" later + self.found_package_json_dep_flag.raise(); } - // Attempt to resolve with the workspace resolver - let result: Result<_, ResolveError> = self - .workspace_resolver - .resolve(raw_specifier, referrer) - .map_err(|err| match err { - MappedResolutionError::Specifier(err) => ResolveError::Specifier(err), - MappedResolutionError::ImportMap(err) => { - ResolveError::Other(err.into()) - } - MappedResolutionError::Workspace(err) => { - ResolveError::Other(err.into()) - } - }); - let result = match result { - Ok(resolution) => match resolution { - MappedResolution::Normal { - specifier, - maybe_diagnostic, - } - | MappedResolution::ImportMap { - specifier, - maybe_diagnostic, - } => { - if let Some(diagnostic) = maybe_diagnostic { - match &*diagnostic { - MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { reference, .. } => { - if self.warned_pkgs.insert(reference.req().clone()) { - log::warn!("{} {}\n at {}", colors::yellow("Warning"), diagnostic, referrer_range); - } - } - } - } - // do sloppy imports resolution if enabled - if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { - Ok( - sloppy_imports_resolver - .resolve( - &specifier, - match mode { - ResolutionMode::Execution => { - SloppyImportsResolutionMode::Execution - } - ResolutionMode::Types => SloppyImportsResolutionMode::Types, - }, - ) - .map(|s| s.into_specifier()) - .unwrap_or(specifier), - ) - } else { - Ok(specifier) - } - } - MappedResolution::WorkspaceJsrPackage { specifier, .. } => { - Ok(specifier) - } - MappedResolution::WorkspaceNpmPackage { - target_pkg_json: pkg_json, - sub_path, - .. - } => self - .node_resolver - .as_ref() - .unwrap() - .resolve_package_sub_path_from_deno_module( - pkg_json.dir_path(), - sub_path.as_deref(), - Some(referrer), - referrer_kind, - to_node_mode(mode), - ) - .map_err(|e| ResolveError::Other(e.into())), - MappedResolution::PackageJson { - dep_result, - alias, - sub_path, + if let Some(diagnostic) = resolution.maybe_diagnostic { + match &*diagnostic { + MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { + reference, .. } => { - // found a specifier in the package.json, so mark that - // we need to do an "npm install" later - self.found_package_json_dep_flag.raise(); - - dep_result - .as_ref() - .map_err(|e| ResolveError::Other(e.clone().into())) - .and_then(|dep| match dep { - PackageJsonDepValue::Req(req) => { - ModuleSpecifier::parse(&format!( - "npm:{}{}", - req, - sub_path.map(|s| format!("/{}", s)).unwrap_or_default() - )) - .map_err(|e| ResolveError::Other(e.into())) - } - PackageJsonDepValue::Workspace(version_req) => self - .workspace_resolver - .resolve_workspace_pkg_json_folder_for_pkg_json_dep( - alias, - version_req, - ) - .map_err(|e| ResolveError::Other(e.into())) - .and_then(|pkg_folder| { - self - .node_resolver - .as_ref() - .unwrap() - .resolve_package_sub_path_from_deno_module( - pkg_folder, - sub_path.as_deref(), - Some(referrer), - referrer_kind, - to_node_mode(mode), - ) - .map_err(|e| ResolveError::Other(e.into())) - }), - }) - } - }, - Err(err) => Err(err), - }; - - // When the user is vendoring, don't allow them to import directly from the vendor/ directory - // as it might cause them confusion or duplicate dependencies. Additionally, this folder has - // special treatment in the language server so it will definitely cause issues/confusion there - // if they do this. - if let Some(vendor_specifier) = &self.maybe_vendor_specifier { - if let Ok(specifier) = &result { - if specifier.as_str().starts_with(vendor_specifier.as_str()) { - return Err(ResolveError::Other(anyhow!("Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring."))); + if self.warned_pkgs.insert(reference.req().clone()) { + log::warn!( + "{} {}\n at {}", + colors::yellow("Warning"), + diagnostic, + referrer_range + ); + } } } } - let Some(node_resolver) = &self.node_resolver else { - return result; - }; - - let is_byonm = self - .npm_resolver - .as_ref() - .is_some_and(|r| r.as_byonm().is_some()); - match result { - Ok(specifier) => { - if let Ok(npm_req_ref) = - NpmPackageReqReference::from_specifier(&specifier) - { - // check if the npm specifier resolves to a workspace member - if let Some(pkg_folder) = self - .workspace_resolver - .resolve_workspace_pkg_json_folder_for_npm_specifier( - npm_req_ref.req(), - ) - { - return node_resolver - .resolve_package_sub_path_from_deno_module( - pkg_folder, - npm_req_ref.sub_path(), - Some(referrer), - referrer_kind, - to_node_mode(mode), - ) - .map_err(|e| ResolveError::Other(e.into())); - } - - // do npm resolution for byonm - if is_byonm { - return node_resolver - .resolve_req_reference( - &npm_req_ref, - referrer, - referrer_kind, - to_node_mode(mode), - ) - .map_err(|err| err.into()); - } - } - - Ok(match node_resolver.handle_if_in_node_modules(&specifier)? { - Some(specifier) => specifier, - None => specifier, - }) - } - Err(err) => { - // If byonm, check if the bare specifier resolves to an npm package - if is_byonm && referrer.scheme() == "file" { - let maybe_resolution = node_resolver - .resolve_if_for_npm_pkg( - raw_specifier, - referrer, - referrer_kind, - to_node_mode(mode), - ) - .map_err(ResolveError::Other)?; - if let Some(res) = maybe_resolution { - match res { - NodeResolution::Module(url) => return Ok(url), - NodeResolution::BuiltIn(_) => { - // don't resolve bare specifiers for built-in modules via node resolution - } - } - } - } - - Err(err) - } - } + Ok(resolution.url) } } diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 15937c7aee..e3449c1520 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -29,6 +29,7 @@ use deno_core::RequestedModuleType; use deno_core::ResolutionKind; use deno_npm::npm_rc::ResolvedNpmRc; use deno_package_json::PackageJsonDepValue; +use deno_resolver::npm::NpmReqResolverOptions; use deno_runtime::deno_fs; use deno_runtime::deno_node::create_host_defined_options; use deno_runtime::deno_node::NodeRequireLoader; @@ -79,7 +80,7 @@ use crate::npm::CliNpmResolverManagedSnapshotOption; use crate::npm::CreateInNpmPkgCheckerOptions; use crate::resolver::CjsTracker; use crate::resolver::CliDenoResolverFs; -use crate::resolver::CliNodeResolver; +use crate::resolver::CliNpmReqResolver; use crate::resolver::IsCjsResolverOptions; use crate::resolver::NpmModuleLoader; use crate::util::progress_bar::ProgressBar; @@ -107,8 +108,9 @@ struct SharedModuleLoaderState { fs: Arc, modules: StandaloneModules, node_code_translator: Arc, - node_resolver: Arc, + node_resolver: Arc, npm_module_loader: Arc, + npm_req_resolver: Arc, npm_resolver: Arc, workspace_resolver: WorkspaceResolver, } @@ -190,7 +192,7 @@ impl ModuleLoader for EmbeddedModuleLoader { self .shared .node_resolver - .resolve_package_sub_path_from_deno_module( + .resolve_package_subpath_from_deno_module( pkg_json.dir_path(), sub_path.as_deref(), Some(&referrer), @@ -204,15 +206,17 @@ impl ModuleLoader for EmbeddedModuleLoader { alias, .. }) => match dep_result.as_ref().map_err(|e| AnyError::from(e.clone()))? { - PackageJsonDepValue::Req(req) => { - self.shared.node_resolver.resolve_req_with_sub_path( + PackageJsonDepValue::Req(req) => self + .shared + .npm_req_resolver + .resolve_req_with_sub_path( req, sub_path.as_deref(), &referrer, referrer_kind, NodeResolutionMode::Execution, ) - } + .map_err(AnyError::from), PackageJsonDepValue::Workspace(version_req) => { let pkg_folder = self .shared @@ -225,7 +229,7 @@ impl ModuleLoader for EmbeddedModuleLoader { self .shared .node_resolver - .resolve_package_sub_path_from_deno_module( + .resolve_package_subpath_from_deno_module( pkg_folder, sub_path.as_deref(), Some(&referrer), @@ -240,12 +244,12 @@ impl ModuleLoader for EmbeddedModuleLoader { if let Ok(reference) = NpmPackageReqReference::from_specifier(&specifier) { - return self.shared.node_resolver.resolve_req_reference( + return Ok(self.shared.npm_req_resolver.resolve_req_reference( &reference, &referrer, referrer_kind, NodeResolutionMode::Execution, - ); + )?); } if specifier.scheme() == "jsr" { @@ -260,14 +264,14 @@ impl ModuleLoader for EmbeddedModuleLoader { self .shared .node_resolver - .handle_if_in_node_modules(&specifier)? + .handle_if_in_node_modules(&specifier) .unwrap_or(specifier), ) } Err(err) if err.is_unmapped_bare_specifier() && referrer.scheme() == "file" => { - let maybe_res = self.shared.node_resolver.resolve_if_for_npm_pkg( + let maybe_res = self.shared.npm_req_resolver.resolve_if_for_npm_pkg( raw_specifier, &referrer, referrer_kind, @@ -651,7 +655,7 @@ pub async fn run(data: StandaloneData) -> Result { let node_resolver = Arc::new(NodeResolver::new( deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), in_npm_pkg_checker.clone(), - npm_resolver.clone().into_npm_resolver(), + npm_resolver.clone().into_npm_pkg_folder_resolver(), pkg_json_resolver.clone(), )); let cjs_tracker = Arc::new(CjsTracker::new( @@ -664,12 +668,14 @@ pub async fn run(data: StandaloneData) -> Result { )); let cache_db = Caches::new(deno_dir_provider.clone()); let node_analysis_cache = NodeAnalysisCache::new(cache_db.node_analysis_db()); - let cli_node_resolver = Arc::new(CliNodeResolver::new( - fs.clone(), - in_npm_pkg_checker.clone(), - node_resolver.clone(), - npm_resolver.clone(), - )); + let npm_req_resolver = + Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { + byonm_resolver: (npm_resolver.clone()).into_maybe_byonm(), + fs: CliDenoResolverFs(fs.clone()), + in_npm_pkg_checker: in_npm_pkg_checker.clone(), + node_resolver: node_resolver.clone(), + npm_req_resolver: npm_resolver.clone().into_npm_req_resolver(), + })); let cjs_esm_code_analyzer = CliCjsCodeAnalyzer::new( node_analysis_cache, cjs_tracker.clone(), @@ -681,7 +687,7 @@ pub async fn run(data: StandaloneData) -> Result { deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), in_npm_pkg_checker, node_resolver.clone(), - npm_resolver.clone().into_npm_resolver(), + npm_resolver.clone().into_npm_pkg_folder_resolver(), pkg_json_resolver.clone(), )); let workspace_resolver = { @@ -739,7 +745,7 @@ pub async fn run(data: StandaloneData) -> Result { fs: fs.clone(), modules, node_code_translator: node_code_translator.clone(), - node_resolver: cli_node_resolver.clone(), + node_resolver: node_resolver.clone(), npm_module_loader: Arc::new(NpmModuleLoader::new( cjs_tracker.clone(), fs.clone(), @@ -747,6 +753,7 @@ pub async fn run(data: StandaloneData) -> Result { )), npm_resolver: npm_resolver.clone(), workspace_resolver, + npm_req_resolver, }), }; diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs index a569061625..452d5c165a 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -6,7 +6,6 @@ use crate::cache::FastInsecureHasher; use crate::cache::ModuleInfoCache; use crate::node; use crate::npm::CliNpmResolver; -use crate::npm::ResolvePkgFolderFromDenoReqError; use crate::resolver::CjsTracker; use crate::util::checksum; use crate::util::path::mapped_specifier_for_tsc; @@ -34,6 +33,7 @@ use deno_graph::GraphKind; use deno_graph::Module; use deno_graph::ModuleGraph; use deno_graph::ResolutionResolved; +use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; use deno_runtime::deno_fs; use deno_runtime::deno_node::NodeResolver; use deno_semver::npm::NpmPackageReqReference; diff --git a/cli/worker.rs b/cli/worker.rs index 83e36b36c2..1afe37e346 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -155,7 +155,7 @@ impl SharedWorkerState { NodeExtInitServices { node_require_loader, node_resolver: self.node_resolver.clone(), - npm_resolver: self.npm_resolver.clone().into_npm_resolver(), + npm_resolver: self.npm_resolver.clone().into_npm_pkg_folder_resolver(), pkg_json_resolver: self.pkg_json_resolver.clone(), } } diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 702c919f47..84f39552c1 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -15,7 +15,7 @@ use deno_core::url::Url; use deno_core::v8; use deno_core::v8::ExternalReference; use node_resolver::errors::ClosestPkgJsonError; -use node_resolver::NpmResolverRc; +use node_resolver::NpmPackageFolderResolverRc; use once_cell::sync::Lazy; extern crate libz_sys as zlib; @@ -183,7 +183,7 @@ fn op_node_build_os() -> String { pub struct NodeExtInitServices { pub node_require_loader: NodeRequireLoaderRc, pub node_resolver: NodeResolverRc, - pub npm_resolver: NpmResolverRc, + pub npm_resolver: NpmPackageFolderResolverRc, pub pkg_json_resolver: PackageJsonResolverRc, } diff --git a/ext/node/ops/require.rs b/ext/node/ops/require.rs index b7fa8feb20..e381ee91d4 100644 --- a/ext/node/ops/require.rs +++ b/ext/node/ops/require.rs @@ -24,7 +24,7 @@ use std::rc::Rc; use crate::NodePermissions; use crate::NodeRequireLoaderRc; use crate::NodeResolverRc; -use crate::NpmResolverRc; +use crate::NpmPackageFolderResolverRc; use crate::PackageJsonResolverRc; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] @@ -220,7 +220,7 @@ pub fn op_require_resolve_deno_dir( #[string] request: String, #[string] parent_filename: String, ) -> Result, AnyError> { - let resolver = state.borrow::(); + let resolver = state.borrow::(); Ok( resolver .resolve_package_folder_from_package( diff --git a/resolvers/deno/Cargo.toml b/resolvers/deno/Cargo.toml index 24d50587b3..89c0232dc9 100644 --- a/resolvers/deno/Cargo.toml +++ b/resolvers/deno/Cargo.toml @@ -16,6 +16,8 @@ path = "lib.rs" [dependencies] anyhow.workspace = true base32.workspace = true +dashmap.workspace = true +deno_config.workspace = true deno_media_type.workspace = true deno_package_json.workspace = true deno_package_json.features = ["sync"] diff --git a/resolvers/deno/cjs.rs b/resolvers/deno/cjs.rs new file mode 100644 index 0000000000..dbcbd8b6bf --- /dev/null +++ b/resolvers/deno/cjs.rs @@ -0,0 +1,272 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::sync::Arc; + +use dashmap::DashMap; +use deno_media_type::MediaType; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::ClosestPkgJsonError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::PackageJsonResolver; +use url::Url; + +/// Keeps track of what module specifiers were resolved as CJS. +/// +/// Modules that are `.js`, `.ts`, `.jsx`, and `tsx` are only known to +/// be CJS or ESM after they're loaded based on their contents. So these +/// files will be "maybe CJS" until they're loaded. +#[derive(Debug)] +pub struct CjsTracker { + is_cjs_resolver: IsCjsResolver, + known: DashMap, +} + +impl CjsTracker { + pub fn new( + in_npm_pkg_checker: Arc, + pkg_json_resolver: Arc>, + options: IsCjsResolverOptions, + ) -> Self { + Self { + is_cjs_resolver: IsCjsResolver::new( + in_npm_pkg_checker, + pkg_json_resolver, + options, + ), + known: Default::default(), + } + } + + /// Checks whether the file might be treated as CJS, but it's not for sure + /// yet because the source hasn't been loaded to see whether it contains + /// imports or exports. + pub fn is_maybe_cjs( + &self, + specifier: &Url, + media_type: MediaType, + ) -> Result { + self.treat_as_cjs_with_is_script(specifier, media_type, None) + } + + /// Gets whether the file is CJS. If true, this is for sure + /// cjs because `is_script` is provided. + /// + /// `is_script` should be `true` when the contents of the file at the + /// provided specifier are known to be a script and not an ES module. + pub fn is_cjs_with_known_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: bool, + ) -> Result { + self.treat_as_cjs_with_is_script(specifier, media_type, Some(is_script)) + } + + fn treat_as_cjs_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option, + ) -> Result { + let kind = match self + .get_known_kind_with_is_script(specifier, media_type, is_script) + { + Some(kind) => kind, + None => self.is_cjs_resolver.check_based_on_pkg_json(specifier)?, + }; + Ok(kind == NodeModuleKind::Cjs) + } + + /// Gets the referrer for the specified module specifier. + /// + /// Generally the referrer should already be tracked by calling + /// `is_cjs_with_known_is_script` before calling this method. + pub fn get_referrer_kind(&self, specifier: &Url) -> NodeModuleKind { + if specifier.scheme() != "file" { + return NodeModuleKind::Esm; + } + self + .get_known_kind(specifier, MediaType::from_specifier(specifier)) + .unwrap_or(NodeModuleKind::Esm) + } + + fn get_known_kind( + &self, + specifier: &Url, + media_type: MediaType, + ) -> Option { + self.get_known_kind_with_is_script(specifier, media_type, None) + } + + fn get_known_kind_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option, + ) -> Option { + self.is_cjs_resolver.get_known_kind_with_is_script( + specifier, + media_type, + is_script, + &self.known, + ) + } +} + +#[derive(Debug)] +pub struct IsCjsResolverOptions { + pub detect_cjs: bool, + pub is_node_main: bool, +} + +/// Resolves whether a module is CJS or ESM. +#[derive(Debug)] +pub struct IsCjsResolver { + in_npm_pkg_checker: Arc, + pkg_json_resolver: Arc>, + options: IsCjsResolverOptions, +} + +impl IsCjsResolver { + pub fn new( + in_npm_pkg_checker: Arc, + pkg_json_resolver: Arc>, + options: IsCjsResolverOptions, + ) -> Self { + Self { + in_npm_pkg_checker, + pkg_json_resolver, + options, + } + } + + /// Gets the referrer kind for a script in the LSP. + pub fn get_lsp_referrer_kind( + &self, + specifier: &Url, + is_script: Option, + ) -> NodeModuleKind { + if specifier.scheme() != "file" { + return NodeModuleKind::Esm; + } + match MediaType::from_specifier(specifier) { + MediaType::Mts | MediaType::Mjs | MediaType::Dmts => NodeModuleKind::Esm, + MediaType::Cjs | MediaType::Cts | MediaType::Dcts => NodeModuleKind::Cjs, + MediaType::Dts => { + // dts files are always determined based on the package.json because + // they contain imports/exports even when considered CJS + self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm) + } + MediaType::Wasm | + MediaType::Json => NodeModuleKind::Esm, + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + // treat these as unknown + | MediaType::Css + | MediaType::SourceMap + | MediaType::Unknown => { + match is_script { + Some(true) => self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm), + Some(false) | None => NodeModuleKind::Esm, + } + } + } + } + + fn get_known_kind_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option, + known_cache: &DashMap, + ) -> Option { + if specifier.scheme() != "file" { + return Some(NodeModuleKind::Esm); + } + + match media_type { + MediaType::Mts | MediaType::Mjs | MediaType::Dmts => Some(NodeModuleKind::Esm), + MediaType::Cjs | MediaType::Cts | MediaType::Dcts => Some(NodeModuleKind::Cjs), + MediaType::Dts => { + // dts files are always determined based on the package.json because + // they contain imports/exports even when considered CJS + if let Some(value) = known_cache.get(specifier).map(|v| *v) { + Some(value) + } else { + let value = self.check_based_on_pkg_json(specifier).ok(); + if let Some(value) = value { + known_cache.insert(specifier.clone(), value); + } + Some(value.unwrap_or(NodeModuleKind::Esm)) + } + } + MediaType::Wasm | + MediaType::Json => Some(NodeModuleKind::Esm), + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + // treat these as unknown + | MediaType::Css + | MediaType::SourceMap + | MediaType::Unknown => { + if let Some(value) = known_cache.get(specifier).map(|v| *v) { + if value == NodeModuleKind::Cjs && is_script == Some(false) { + // we now know this is actually esm + known_cache.insert(specifier.clone(), NodeModuleKind::Esm); + Some(NodeModuleKind::Esm) + } else { + Some(value) + } + } else if is_script == Some(false) { + // we know this is esm + known_cache.insert(specifier.clone(), NodeModuleKind::Esm); + Some(NodeModuleKind::Esm) + } else { + None + } + } + } + } + + fn check_based_on_pkg_json( + &self, + specifier: &Url, + ) -> Result { + if self.in_npm_pkg_checker.in_npm_package(specifier) { + if let Some(pkg_json) = + self.pkg_json_resolver.get_closest_package_json(specifier)? + { + let is_file_location_cjs = pkg_json.typ != "module"; + Ok(if is_file_location_cjs { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }) + } else { + Ok(NodeModuleKind::Cjs) + } + } else if self.options.detect_cjs || self.options.is_node_main { + if let Some(pkg_json) = + self.pkg_json_resolver.get_closest_package_json(specifier)? + { + let is_cjs_type = pkg_json.typ == "commonjs" + || self.options.is_node_main && pkg_json.typ == "none"; + Ok(if is_cjs_type { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }) + } else if self.options.is_node_main { + Ok(NodeModuleKind::Cjs) + } else { + Ok(NodeModuleKind::Esm) + } + } else { + Ok(NodeModuleKind::Esm) + } + } +} diff --git a/resolvers/deno/fs.rs b/resolvers/deno/fs.rs index 44495fa7c2..4929f4508e 100644 --- a/resolvers/deno/fs.rs +++ b/resolvers/deno/fs.rs @@ -12,6 +12,7 @@ pub struct DirEntry { pub trait DenoResolverFs { fn read_to_string_lossy(&self, path: &Path) -> std::io::Result; fn realpath_sync(&self, path: &Path) -> std::io::Result; + fn exists_sync(&self, path: &Path) -> bool; fn is_dir_sync(&self, path: &Path) -> bool; fn read_dir_sync(&self, dir_path: &Path) -> std::io::Result>; } diff --git a/resolvers/deno/lib.rs b/resolvers/deno/lib.rs index 57fa67512a..a2b6b642fc 100644 --- a/resolvers/deno/lib.rs +++ b/resolvers/deno/lib.rs @@ -1,5 +1,439 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#![deny(clippy::print_stderr)] +#![deny(clippy::print_stdout)] + +use std::path::PathBuf; +use std::sync::Arc; + +use deno_config::workspace::MappedResolution; +use deno_config::workspace::MappedResolutionDiagnostic; +use deno_config::workspace::MappedResolutionError; +use deno_config::workspace::WorkspaceResolvePkgJsonFolderError; +use deno_config::workspace::WorkspaceResolver; +use deno_package_json::PackageJsonDepValue; +use deno_package_json::PackageJsonDepValueParseError; +use deno_semver::npm::NpmPackageReqReference; +use fs::DenoResolverFs; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::NodeResolveError; +use node_resolver::errors::PackageSubpathResolveError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::NodeResolution; +use node_resolver::NodeResolutionMode; +use node_resolver::NodeResolver; +use npm::MissingPackageNodeModulesFolderError; +use npm::NodeModulesOutOfDateError; +use npm::NpmReqResolver; +use npm::ResolveIfForNpmPackageError; +use npm::ResolvePkgFolderFromDenoReqError; +use npm::ResolveReqWithSubPathError; +use sloppy_imports::SloppyImportResolverFs; +use sloppy_imports::SloppyImportsResolutionMode; +use sloppy_imports::SloppyImportsResolver; +use thiserror::Error; +use url::Url; + +pub mod cjs; pub mod fs; pub mod npm; pub mod sloppy_imports; + +#[derive(Debug, Clone)] +pub struct DenoResolution { + pub url: Url, + pub maybe_diagnostic: Option>, + pub found_package_json_dep: bool, +} + +#[derive(Debug, Error)] +pub enum DenoResolveErrorKind { + #[error("Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring.")] + InvalidVendorFolderImport, + #[error(transparent)] + MappedResolution(#[from] MappedResolutionError), + #[error(transparent)] + MissingPackageNodeModulesFolder(#[from] MissingPackageNodeModulesFolderError), + #[error(transparent)] + Node(#[from] NodeResolveError), + #[error(transparent)] + NodeModulesOutOfDate(#[from] NodeModulesOutOfDateError), + #[error(transparent)] + PackageJsonDepValueParse(#[from] PackageJsonDepValueParseError), + #[error(transparent)] + PackageJsonDepValueUrlParse(url::ParseError), + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), + #[error(transparent)] + ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError), + #[error(transparent)] + WorkspaceResolvePkgJsonFolder(#[from] WorkspaceResolvePkgJsonFolderError), +} + +impl DenoResolveErrorKind { + pub fn into_box(self) -> DenoResolveError { + DenoResolveError(Box::new(self)) + } +} + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct DenoResolveError(pub Box); + +impl DenoResolveError { + pub fn as_kind(&self) -> &DenoResolveErrorKind { + &self.0 + } + + pub fn into_kind(self) -> DenoResolveErrorKind { + *self.0 + } +} + +impl From for DenoResolveError +where + DenoResolveErrorKind: From, +{ + fn from(err: E) -> Self { + DenoResolveError(Box::new(DenoResolveErrorKind::from(err))) + } +} + +#[derive(Debug)] +pub struct NodeAndNpmReqResolver< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, +> { + pub node_resolver: Arc>, + pub npm_req_resolver: Arc>, +} + +pub struct DenoResolverOptions< + 'a, + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, +> { + pub in_npm_pkg_checker: Arc, + pub node_and_req_resolver: + Option>, + pub sloppy_imports_resolver: + Option>>, + pub workspace_resolver: Arc, + /// Whether "bring your own node_modules" is enabled where Deno does not + /// setup the node_modules directories automatically, but instead uses + /// what already exists on the file system. + pub is_byonm: bool, + pub maybe_vendor_dir: Option<&'a PathBuf>, +} + +/// A resolver that takes care of resolution, taking into account loaded +/// import map, JSX settings. +#[derive(Debug)] +pub struct DenoResolver< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, +> { + in_npm_pkg_checker: Arc, + node_and_npm_resolver: Option>, + sloppy_imports_resolver: + Option>>, + workspace_resolver: Arc, + is_byonm: bool, + maybe_vendor_specifier: Option, +} + +impl< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, + > DenoResolver +{ + pub fn new( + options: DenoResolverOptions, + ) -> Self { + Self { + in_npm_pkg_checker: options.in_npm_pkg_checker, + node_and_npm_resolver: options.node_and_req_resolver, + sloppy_imports_resolver: options.sloppy_imports_resolver, + workspace_resolver: options.workspace_resolver, + is_byonm: options.is_byonm, + maybe_vendor_specifier: options + .maybe_vendor_dir + .and_then(|v| deno_path_util::url_from_directory_path(v).ok()), + } + } + + pub fn resolve( + &self, + raw_specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result { + let mut found_package_json_dep = false; + let mut maybe_diagnostic = None; + // Use node resolution if we're in an npm package + if let Some(node_and_npm_resolver) = self.node_and_npm_resolver.as_ref() { + let node_resolver = &node_and_npm_resolver.node_resolver; + if referrer.scheme() == "file" + && self.in_npm_pkg_checker.in_npm_package(referrer) + { + return node_resolver + .resolve(raw_specifier, referrer, referrer_kind, mode) + .map(|res| DenoResolution { + url: res.into_url(), + found_package_json_dep, + maybe_diagnostic, + }) + .map_err(|e| e.into()); + } + } + + // Attempt to resolve with the workspace resolver + let result: Result<_, DenoResolveError> = self + .workspace_resolver + .resolve(raw_specifier, referrer) + .map_err(|err| err.into()); + let result = match result { + Ok(resolution) => match resolution { + MappedResolution::Normal { + specifier, + maybe_diagnostic: current_diagnostic, + } + | MappedResolution::ImportMap { + specifier, + maybe_diagnostic: current_diagnostic, + } => { + maybe_diagnostic = current_diagnostic; + // do sloppy imports resolution if enabled + if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { + Ok( + sloppy_imports_resolver + .resolve( + &specifier, + match mode { + NodeResolutionMode::Execution => { + SloppyImportsResolutionMode::Execution + } + NodeResolutionMode::Types => { + SloppyImportsResolutionMode::Types + } + }, + ) + .map(|s| s.into_specifier()) + .unwrap_or(specifier), + ) + } else { + Ok(specifier) + } + } + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + Ok(specifier) + } + MappedResolution::WorkspaceNpmPackage { + target_pkg_json: pkg_json, + sub_path, + .. + } => self + .node_and_npm_resolver + .as_ref() + .unwrap() + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_json.dir_path(), + sub_path.as_deref(), + Some(referrer), + referrer_kind, + mode, + ) + .map_err(|e| e.into()), + MappedResolution::PackageJson { + dep_result, + alias, + sub_path, + .. + } => { + // found a specifier in the package.json, so mark that + // we need to do an "npm install" later + found_package_json_dep = true; + + dep_result + .as_ref() + .map_err(|e| { + DenoResolveErrorKind::PackageJsonDepValueParse(e.clone()) + .into_box() + }) + .and_then(|dep| match dep { + // todo(dsherret): it seems bad that we're converting this + // to a url because the req might not be a valid url. + PackageJsonDepValue::Req(req) => Url::parse(&format!( + "npm:{}{}", + req, + sub_path.map(|s| format!("/{}", s)).unwrap_or_default() + )) + .map_err(|e| { + DenoResolveErrorKind::PackageJsonDepValueUrlParse(e).into_box() + }), + PackageJsonDepValue::Workspace(version_req) => self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_pkg_json_dep( + alias, + version_req, + ) + .map_err(|e| { + DenoResolveErrorKind::WorkspaceResolvePkgJsonFolder(e) + .into_box() + }) + .and_then(|pkg_folder| { + self + .node_and_npm_resolver + .as_ref() + .unwrap() + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_folder, + sub_path.as_deref(), + Some(referrer), + referrer_kind, + mode, + ) + .map_err(|e| { + DenoResolveErrorKind::PackageSubpathResolve(e).into_box() + }) + }), + }) + } + }, + Err(err) => Err(err), + }; + + // When the user is vendoring, don't allow them to import directly from the vendor/ directory + // as it might cause them confusion or duplicate dependencies. Additionally, this folder has + // special treatment in the language server so it will definitely cause issues/confusion there + // if they do this. + if let Some(vendor_specifier) = &self.maybe_vendor_specifier { + if let Ok(specifier) = &result { + if specifier.as_str().starts_with(vendor_specifier.as_str()) { + return Err( + DenoResolveErrorKind::InvalidVendorFolderImport.into_box(), + ); + } + } + } + + let Some(NodeAndNpmReqResolver { + node_resolver, + npm_req_resolver, + }) = &self.node_and_npm_resolver + else { + return Ok(DenoResolution { + url: result?, + maybe_diagnostic, + found_package_json_dep, + }); + }; + + match result { + Ok(specifier) => { + if let Ok(npm_req_ref) = + NpmPackageReqReference::from_specifier(&specifier) + { + // check if the npm specifier resolves to a workspace member + if let Some(pkg_folder) = self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_npm_specifier( + npm_req_ref.req(), + ) + { + return node_resolver + .resolve_package_subpath_from_deno_module( + pkg_folder, + npm_req_ref.sub_path(), + Some(referrer), + referrer_kind, + mode, + ) + .map(|url| DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + .map_err(|e| e.into()); + } + + // do npm resolution for byonm + if self.is_byonm { + return npm_req_resolver + .resolve_req_reference( + &npm_req_ref, + referrer, + referrer_kind, + mode, + ) + .map(|url| DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + .map_err(|err| match err { + ResolveReqWithSubPathError::MissingPackageNodeModulesFolder( + err, + ) => err.into(), + ResolveReqWithSubPathError::ResolvePkgFolderFromDenoReq( + err, + ) => err.into(), + ResolveReqWithSubPathError::PackageSubpathResolve(err) => { + err.into() + } + }); + } + } + + Ok(DenoResolution { + url: node_resolver + .handle_if_in_node_modules(&specifier) + .unwrap_or(specifier), + maybe_diagnostic, + found_package_json_dep, + }) + } + Err(err) => { + // If byonm, check if the bare specifier resolves to an npm package + if self.is_byonm && referrer.scheme() == "file" { + let maybe_resolution = npm_req_resolver + .resolve_if_for_npm_pkg( + raw_specifier, + referrer, + referrer_kind, + mode, + ) + .map_err(|e| match e { + ResolveIfForNpmPackageError::NodeResolve(e) => { + DenoResolveErrorKind::Node(e).into_box() + } + ResolveIfForNpmPackageError::NodeModulesOutOfDate(e) => e.into(), + })?; + if let Some(res) = maybe_resolution { + match res { + NodeResolution::Module(url) => { + return Ok(DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + } + NodeResolution::BuiltIn(_) => { + // don't resolve bare specifiers for built-in modules via node resolution + } + } + } + } + + Err(err) + } + } + } +} diff --git a/resolvers/deno/npm/byonm.rs b/resolvers/deno/npm/byonm.rs index b85117052c..e9182d47a1 100644 --- a/resolvers/deno/npm/byonm.rs +++ b/resolvers/deno/npm/byonm.rs @@ -16,7 +16,7 @@ use node_resolver::errors::PackageFolderResolveIoError; use node_resolver::errors::PackageJsonLoadError; use node_resolver::errors::PackageNotFoundError; use node_resolver::InNpmPackageChecker; -use node_resolver::NpmResolver; +use node_resolver::NpmPackageFolderResolver; use node_resolver::PackageJsonResolverRc; use thiserror::Error; use url::Url; @@ -24,6 +24,8 @@ use url::Url; use crate::fs::DenoResolverFs; use super::local::normalize_pkg_name_for_node_modules_deno_folder; +use super::CliNpmReqResolver; +use super::ResolvePkgFolderFromDenoReqError; #[derive(Debug, Error)] pub enum ByonmResolvePkgFolderFromDenoReqError { @@ -303,7 +305,24 @@ impl ByonmNpmResolver { impl< Fs: DenoResolverFs + Send + Sync + std::fmt::Debug, TEnv: NodeResolverEnv, - > NpmResolver for ByonmNpmResolver + > CliNpmReqResolver for ByonmNpmResolver +{ + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result { + ByonmNpmResolver::resolve_pkg_folder_from_deno_module_req( + self, req, referrer, + ) + .map_err(ResolvePkgFolderFromDenoReqError::Byonm) + } +} + +impl< + Fs: DenoResolverFs + Send + Sync + std::fmt::Debug, + TEnv: NodeResolverEnv, + > NpmPackageFolderResolver for ByonmNpmResolver { fn resolve_package_folder_from_package( &self, diff --git a/resolvers/deno/npm/mod.rs b/resolvers/deno/npm/mod.rs index 45e2341c78..b0aec71b08 100644 --- a/resolvers/deno/npm/mod.rs +++ b/resolvers/deno/npm/mod.rs @@ -1,10 +1,256 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -mod byonm; -mod local; +use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_semver::npm::NpmPackageReqReference; +use deno_semver::package::PackageReq; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::NodeResolveError; +use node_resolver::errors::NodeResolveErrorKind; +use node_resolver::errors::PackageFolderResolveErrorKind; +use node_resolver::errors::PackageFolderResolveIoError; +use node_resolver::errors::PackageNotFoundError; +use node_resolver::errors::PackageResolveErrorKind; +use node_resolver::errors::PackageSubpathResolveError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::NodeResolution; +use node_resolver::NodeResolutionMode; +use node_resolver::NodeResolver; +use thiserror::Error; +use url::Url; + +use crate::fs::DenoResolverFs; pub use byonm::ByonmInNpmPackageChecker; pub use byonm::ByonmNpmResolver; pub use byonm::ByonmNpmResolverCreateOptions; pub use byonm::ByonmResolvePkgFolderFromDenoReqError; pub use local::normalize_pkg_name_for_node_modules_deno_folder; + +mod byonm; +mod local; + +#[derive(Debug, Error)] +#[error("Could not resolve \"{}\", but found it in a package.json. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", specifier)] +pub struct NodeModulesOutOfDateError { + pub specifier: String, +} + +#[derive(Debug, Error)] +#[error("Could not find '{}'. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", package_json_path.display())] +pub struct MissingPackageNodeModulesFolderError { + pub package_json_path: PathBuf, +} + +#[derive(Debug, Error)] +pub enum ResolveIfForNpmPackageError { + #[error(transparent)] + NodeResolve(#[from] NodeResolveError), + #[error(transparent)] + NodeModulesOutOfDate(#[from] NodeModulesOutOfDateError), +} + +#[derive(Debug, Error)] +pub enum ResolveReqWithSubPathError { + #[error(transparent)] + MissingPackageNodeModulesFolder(#[from] MissingPackageNodeModulesFolderError), + #[error(transparent)] + ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError), + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), +} + +#[derive(Debug, Error)] +pub enum ResolvePkgFolderFromDenoReqError { + // todo(dsherret): don't use anyhow here + #[error(transparent)] + Managed(anyhow::Error), + #[error(transparent)] + Byonm(#[from] ByonmResolvePkgFolderFromDenoReqError), +} + +// todo(dsherret): a temporary trait until we extract +// out the CLI npm resolver into here +pub trait CliNpmReqResolver: Debug + Send + Sync { + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result; +} + +pub struct NpmReqResolverOptions< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, +> { + /// The resolver when "bring your own node_modules" is enabled where Deno + /// does not setup the node_modules directories automatically, but instead + /// uses what already exists on the file system. + pub byonm_resolver: Option>>, + pub fs: Fs, + pub in_npm_pkg_checker: Arc, + pub node_resolver: Arc>, + pub npm_req_resolver: Arc, +} + +#[derive(Debug)] +pub struct NpmReqResolver +{ + byonm_resolver: Option>>, + fs: Fs, + in_npm_pkg_checker: Arc, + node_resolver: Arc>, + npm_resolver: Arc, +} + +impl + NpmReqResolver +{ + pub fn new(options: NpmReqResolverOptions) -> Self { + Self { + byonm_resolver: options.byonm_resolver, + fs: options.fs, + in_npm_pkg_checker: options.in_npm_pkg_checker, + node_resolver: options.node_resolver, + npm_resolver: options.npm_req_resolver, + } + } + + pub fn resolve_req_reference( + &self, + req_ref: &NpmPackageReqReference, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result { + self.resolve_req_with_sub_path( + req_ref.req(), + req_ref.sub_path(), + referrer, + referrer_kind, + mode, + ) + } + + pub fn resolve_req_with_sub_path( + &self, + req: &PackageReq, + sub_path: Option<&str>, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result { + let package_folder = self + .npm_resolver + .resolve_pkg_folder_from_deno_module_req(req, referrer)?; + let resolution_result = + self.node_resolver.resolve_package_subpath_from_deno_module( + &package_folder, + sub_path, + Some(referrer), + referrer_kind, + mode, + ); + match resolution_result { + Ok(url) => Ok(url), + Err(err) => { + if self.byonm_resolver.is_some() { + let package_json_path = package_folder.join("package.json"); + if !self.fs.exists_sync(&package_json_path) { + return Err( + MissingPackageNodeModulesFolderError { package_json_path }.into(), + ); + } + } + Err(err.into()) + } + } + } + + pub fn resolve_if_for_npm_pkg( + &self, + specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result, ResolveIfForNpmPackageError> { + let resolution_result = + self + .node_resolver + .resolve(specifier, referrer, referrer_kind, mode); + match resolution_result { + Ok(res) => Ok(Some(res)), + Err(err) => { + let err = err.into_kind(); + match err { + NodeResolveErrorKind::RelativeJoin(_) + | NodeResolveErrorKind::PackageImportsResolve(_) + | NodeResolveErrorKind::UnsupportedEsmUrlScheme(_) + | NodeResolveErrorKind::DataUrlReferrer(_) + | NodeResolveErrorKind::TypesNotFound(_) + | NodeResolveErrorKind::FinalizeResolution(_) => { + Err(ResolveIfForNpmPackageError::NodeResolve(err.into())) + } + NodeResolveErrorKind::PackageResolve(err) => { + let err = err.into_kind(); + match err { + PackageResolveErrorKind::ClosestPkgJson(_) + | PackageResolveErrorKind::InvalidModuleSpecifier(_) + | PackageResolveErrorKind::ExportsResolve(_) + | PackageResolveErrorKind::SubpathResolve(_) => { + Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )) + } + PackageResolveErrorKind::PackageFolderResolve(err) => { + match err.as_kind() { + PackageFolderResolveErrorKind::Io( + PackageFolderResolveIoError { package_name, .. }, + ) + | PackageFolderResolveErrorKind::PackageNotFound( + PackageNotFoundError { package_name, .. }, + ) => { + if self.in_npm_pkg_checker.in_npm_package(referrer) { + return Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )); + } + if let Some(byonm_npm_resolver) = &self.byonm_resolver { + if byonm_npm_resolver + .find_ancestor_package_json_with_dep( + package_name, + referrer, + ) + .is_some() + { + return Err( + ResolveIfForNpmPackageError::NodeModulesOutOfDate( + NodeModulesOutOfDateError { + specifier: specifier.to_string(), + }, + ), + ); + } + } + Ok(None) + } + PackageFolderResolveErrorKind::ReferrerNotFound(_) => { + if self.in_npm_pkg_checker.in_npm_package(referrer) { + return Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )); + } + Ok(None) + } + } + } + } + } + } + } + } + } +} diff --git a/resolvers/node/analyze.rs b/resolvers/node/analyze.rs index c7415933d7..9126890805 100644 --- a/resolvers/node/analyze.rs +++ b/resolvers/node/analyze.rs @@ -23,7 +23,7 @@ use crate::npm::InNpmPackageCheckerRc; use crate::resolution::NodeResolverRc; use crate::NodeModuleKind; use crate::NodeResolutionMode; -use crate::NpmResolverRc; +use crate::NpmPackageFolderResolverRc; use crate::PackageJsonResolverRc; use crate::PathClean; @@ -66,7 +66,7 @@ pub struct NodeCodeTranslator< env: TNodeResolverEnv, in_npm_pkg_checker: InNpmPackageCheckerRc, node_resolver: NodeResolverRc, - npm_resolver: NpmResolverRc, + npm_resolver: NpmPackageFolderResolverRc, pkg_json_resolver: PackageJsonResolverRc, } @@ -78,7 +78,7 @@ impl env: TNodeResolverEnv, in_npm_pkg_checker: InNpmPackageCheckerRc, node_resolver: NodeResolverRc, - npm_resolver: NpmResolverRc, + npm_resolver: NpmPackageFolderResolverRc, pkg_json_resolver: PackageJsonResolverRc, ) -> Self { Self { diff --git a/resolvers/node/lib.rs b/resolvers/node/lib.rs index 18b0a85363..87bd629946 100644 --- a/resolvers/node/lib.rs +++ b/resolvers/node/lib.rs @@ -15,13 +15,14 @@ mod sync; pub use deno_package_json::PackageJson; pub use npm::InNpmPackageChecker; pub use npm::InNpmPackageCheckerRc; -pub use npm::NpmResolver; -pub use npm::NpmResolverRc; +pub use npm::NpmPackageFolderResolver; +pub use npm::NpmPackageFolderResolverRc; pub use package_json::PackageJsonResolver; pub use package_json::PackageJsonResolverRc; pub use package_json::PackageJsonThreadLocalCache; pub use path::PathClean; pub use resolution::parse_npm_pkg_name; +pub use resolution::resolve_specifier_into_node_modules; pub use resolution::NodeModuleKind; pub use resolution::NodeResolution; pub use resolution::NodeResolutionMode; diff --git a/resolvers/node/npm.rs b/resolvers/node/npm.rs index 2132f0b545..ab3a179426 100644 --- a/resolvers/node/npm.rs +++ b/resolvers/node/npm.rs @@ -13,10 +13,13 @@ use crate::sync::MaybeSend; use crate::sync::MaybeSync; #[allow(clippy::disallowed_types)] -pub type NpmResolverRc = crate::sync::MaybeArc; +pub type NpmPackageFolderResolverRc = + crate::sync::MaybeArc; -pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync { - /// Resolves an npm package folder path from an npm package referrer. +pub trait NpmPackageFolderResolver: + std::fmt::Debug + MaybeSend + MaybeSync +{ + /// Resolves an npm package folder path from the specified referrer. fn resolve_package_folder_from_package( &self, specifier: &str, diff --git a/resolvers/node/resolution.rs b/resolvers/node/resolution.rs index fcff292425..673a61abe6 100644 --- a/resolvers/node/resolution.rs +++ b/resolvers/node/resolution.rs @@ -41,7 +41,7 @@ use crate::errors::TypesNotFoundErrorData; use crate::errors::UnsupportedDirImportError; use crate::errors::UnsupportedEsmUrlSchemeError; use crate::npm::InNpmPackageCheckerRc; -use crate::NpmResolverRc; +use crate::NpmPackageFolderResolverRc; use crate::PackageJsonResolverRc; use crate::PathClean; use deno_package_json::PackageJson; @@ -101,7 +101,7 @@ pub type NodeResolverRc = crate::sync::MaybeArc>; pub struct NodeResolver { env: TEnv, in_npm_pkg_checker: InNpmPackageCheckerRc, - npm_resolver: NpmResolverRc, + npm_pkg_folder_resolver: NpmPackageFolderResolverRc, pkg_json_resolver: PackageJsonResolverRc, } @@ -109,13 +109,13 @@ impl NodeResolver { pub fn new( env: TEnv, in_npm_pkg_checker: InNpmPackageCheckerRc, - npm_resolver: NpmResolverRc, + npm_pkg_folder_resolver: NpmPackageFolderResolverRc, pkg_json_resolver: PackageJsonResolverRc, ) -> Self { Self { env, in_npm_pkg_checker, - npm_resolver, + npm_pkg_folder_resolver, pkg_json_resolver, } } @@ -1126,7 +1126,7 @@ impl NodeResolver { mode: NodeResolutionMode, ) -> Result { let package_dir_path = self - .npm_resolver + .npm_pkg_folder_resolver .resolve_package_folder_from_package(package_name, referrer)?; // todo: error with this instead when can't find package @@ -1412,6 +1412,25 @@ impl NodeResolver { ) } } + + /// Resolves a specifier that is pointing into a node_modules folder by canonicalizing it. + /// + /// Returns `None` when the specifier is not in a node_modules folder. + pub fn handle_if_in_node_modules(&self, specifier: &Url) -> Option { + // skip canonicalizing if we definitely know it's unnecessary + if specifier.scheme() == "file" + && specifier.path().contains("/node_modules/") + { + // Specifiers in the node_modules directory are canonicalized + // so canoncalize then check if it's in the node_modules directory. + let specifier = resolve_specifier_into_node_modules(specifier, &|path| { + self.env.realpath_sync(path) + }); + return Some(specifier); + } + + None + } } fn resolve_bin_entry_value<'a>( @@ -1660,6 +1679,28 @@ pub fn parse_npm_pkg_name( Ok((package_name, package_subpath, is_scoped)) } +/// Resolves a specifier that is pointing into a node_modules folder. +/// +/// Note: This should be called whenever getting the specifier from +/// a Module::External(module) reference because that module might +/// not be fully resolved at the time deno_graph is analyzing it +/// because the node_modules folder might not exist at that time. +pub fn resolve_specifier_into_node_modules( + specifier: &Url, + canonicalize: &impl Fn(&Path) -> std::io::Result, +) -> Url { + deno_path_util::url_to_file_path(specifier) + .ok() + // this path might not exist at the time the graph is being created + // because the node_modules folder might not yet exist + .and_then(|path| { + deno_path_util::canonicalize_path_maybe_not_exists(&path, canonicalize) + .ok() + }) + .and_then(|path| deno_path_util::url_from_file_path(&path).ok()) + .unwrap_or_else(|| specifier.clone()) +} + fn pattern_key_compare(a: &str, b: &str) -> i32 { let a_pattern_index = a.find('*'); let b_pattern_index = b.find('*');