From bc8554878eef0a4181c36e07c271423bec72827f Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Wed, 5 Feb 2025 23:08:10 +0000 Subject: [PATCH] fix(check): support sloppy imports with "compilerOptions.rootDirs" (#27973) --- Cargo.lock | 10 +- Cargo.toml | 3 +- cli/factory.rs | 26 +- cli/graph_util.rs | 13 +- cli/lib/standalone/binary.rs | 2 +- cli/lsp/analysis.rs | 7 +- cli/lsp/config.rs | 103 +- cli/lsp/diagnostics.rs | 25 +- cli/lsp/resolver.rs | 9 +- cli/resolver.rs | 10 +- cli/rt/run.rs | 38 +- cli/standalone/binary.rs | 2 +- cli/tools/info.rs | 13 +- cli/tools/lint/mod.rs | 4 +- cli/tools/lint/rules/mod.rs | 13 +- cli/tools/lint/rules/no_sloppy_imports.rs | 74 +- cli/tools/registry/mod.rs | 1 - cli/tools/registry/unfurl.rs | 83 +- resolvers/deno/Cargo.toml | 5 + resolvers/deno/factory.rs | 68 +- resolvers/deno/lib.rs | 56 +- resolvers/deno/sloppy_imports.rs | 582 ---- resolvers/deno/workspace.rs | 2812 +++++++++++++++++ resolvers/node/resolution.rs | 9 - .../__test__.jsonc | 4 + .../deno.json | 6 + .../subdir/mod.ts | 3 + .../subdir_types/import.ts | 1 + 28 files changed, 3043 insertions(+), 939 deletions(-) delete mode 100644 resolvers/deno/sloppy_imports.rs create mode 100644 resolvers/deno/workspace.rs create mode 100644 tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/__test__.jsonc create mode 100644 tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/deno.json create mode 100644 tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir/mod.ts create mode 100644 tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir_types/import.ts diff --git a/Cargo.lock b/Cargo.lock index 5e41e795c57e75..cef572f6869505 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1579,9 +1579,9 @@ dependencies = [ [[package]] name = "deno_config" -version = "0.47.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7883c48549bab8e446a58c64ee3d106a13052d2ff5e864de765a60260cb02b" +checksum = "6c486df63f7fa0f2142c7eba286c7be87a3cd8c93f66f744fb5853a77cf4347b" dependencies = [ "boxed_error", "capacity_builder 0.5.0", @@ -2382,10 +2382,14 @@ dependencies = [ "deno_semver", "deno_terminal 0.2.0", "futures", + "import_map", + "indexmap 2.3.0", "log", "node_resolver", "once_cell", "parking_lot", + "serde", + "serde_json", "sys_traits", "test_server", "thiserror 2.0.3", @@ -8033,6 +8037,8 @@ dependencies = [ "getrandom", "libc", "parking_lot", + "serde", + "serde_json", "windows-sys 0.59.0", ] diff --git a/Cargo.toml b/Cargo.toml index 5af9ac3a88980d..99a2e0812dfa9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ deno_ast = { version = "=0.44.0", features = ["transpiling"] } deno_core = { version = "0.333.0" } deno_bench_util = { version = "0.183.0", path = "./bench_util" } -deno_config = { version = "=0.47.1", features = ["workspace"] } +deno_config = { version = "=0.48.0", features = ["workspace"] } deno_lockfile = "=0.24.0" deno_media_type = { version = "=0.2.5", features = ["module_specifier"] } deno_npm = "=0.27.2" @@ -155,6 +155,7 @@ hyper = { version = "1.6.0", features = ["full"] } hyper-rustls = { version = "0.27.2", default-features = false, features = ["http1", "http2", "tls12", "ring"] } hyper-util = { version = "0.1.10", features = ["tokio", "client", "client-legacy", "server", "server-auto"] } hyper_v014 = { package = "hyper", version = "0.14.26", features = ["runtime", "http1"] } +import_map = { version = "0.21.0", features = ["ext"] } indexmap = { version = "2", features = ["serde"] } ipnet = "2.3" jsonc-parser = { version = "=0.26.2", features = ["serde"] } diff --git a/cli/factory.rs b/cli/factory.rs index bd9b58073f654a..1ae4954f69ae30 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use deno_cache_dir::npm::NpmCacheDir; use deno_config::workspace::Workspace; use deno_config::workspace::WorkspaceDirectory; -use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures::FutureExt; @@ -38,6 +37,7 @@ use deno_resolver::factory::ResolverFactoryOptions; use deno_resolver::factory::SpecifiedImportMapProvider; use deno_resolver::npm::managed::NpmResolutionCell; use deno_resolver::npm::DenoInNpmPackageChecker; +use deno_resolver::workspace::WorkspaceResolver; use deno_runtime::deno_fs; use deno_runtime::deno_fs::RealFs; use deno_runtime::deno_permissions::Permissions; @@ -97,7 +97,6 @@ use crate::resolver::CliDenoResolver; use crate::resolver::CliNpmGraphResolver; use crate::resolver::CliNpmReqResolver; use crate::resolver::CliResolver; -use crate::resolver::CliSloppyImportsResolver; use crate::resolver::FoundPackageJsonDepFlag; use crate::standalone::binary::DenoCompileBinaryWriter; use crate::sys::CliSys; @@ -160,7 +159,8 @@ struct CliSpecifiedImportMapProvider { impl SpecifiedImportMapProvider for CliSpecifiedImportMapProvider { async fn get( &self, - ) -> Result, AnyError> { + ) -> Result, AnyError> + { async fn resolve_import_map_value_from_specifier( specifier: &Url, file_fetcher: &CliFileFetcher, @@ -189,7 +189,7 @@ impl SpecifiedImportMapProvider for CliSpecifiedImportMapProvider { .with_context(|| { format!("Unable to load '{}' import map", specifier) })?; - Ok(Some(deno_config::workspace::SpecifiedImportMap { + Ok(Some(deno_resolver::workspace::SpecifiedImportMap { base_url: specifier, value, })) @@ -199,7 +199,7 @@ impl SpecifiedImportMapProvider for CliSpecifiedImportMapProvider { self.workspace_external_import_map_loader.get_or_load()? { let path_url = deno_path_util::url_from_file_path(&import_map.path)?; - Ok(Some(deno_config::workspace::SpecifiedImportMap { + Ok(Some(deno_resolver::workspace::SpecifiedImportMap { base_url: path_url, value: import_map.value.clone(), })) @@ -646,7 +646,6 @@ impl CliFactory { ResolverFactoryOptions { conditions_from_resolution_mode: Default::default(), node_resolution_cache: Some(Arc::new(NodeResolutionThreadLocalCache)), - no_sloppy_imports_cache: false, npm_system_info: self.flags.subcommand.npm_system_info(), specified_import_map: Some(Box::new(CliSpecifiedImportMapProvider { cli_options: self.cli_options()?.clone(), @@ -663,7 +662,7 @@ impl CliFactory { DenoSubcommand::Publish(_) => { // the node_modules directory is not published to jsr, so resolve // dependencies via the package.json rather than using node resolution - Some(deno_config::workspace::PackageJsonDepResolution::Enabled) + Some(deno_resolver::workspace::PackageJsonDepResolution::Enabled) } _ => None, }, @@ -672,12 +671,6 @@ impl CliFactory { }) } - pub fn sloppy_imports_resolver( - &self, - ) -> Result>, AnyError> { - self.resolver_factory()?.sloppy_imports_resolver() - } - pub fn workspace(&self) -> Result<&Arc, AnyError> { Ok(&self.workspace_directory()?.workspace) } @@ -790,10 +783,9 @@ impl CliFactory { } pub async fn lint_rule_provider(&self) -> Result { - Ok(LintRuleProvider::new( - self.sloppy_imports_resolver()?.cloned(), - Some(self.workspace_resolver().await?.clone()), - )) + Ok(LintRuleProvider::new(Some( + self.workspace_resolver().await?.clone(), + ))) } pub async fn node_resolver(&self) -> Result<&Arc, AnyError> { diff --git a/cli/graph_util.rs b/cli/graph_util.rs index 6a640b7cb01f19..861fa664bff05f 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -34,8 +34,7 @@ use deno_graph::SpecifierError; use deno_graph::WorkspaceFastCheckOption; use deno_path_util::url_to_file_path; use deno_resolver::npm::DenoInNpmPackageChecker; -use deno_resolver::sloppy_imports::SloppyImportsCachedFs; -use deno_resolver::sloppy_imports::SloppyImportsResolutionKind; +use deno_resolver::workspace::sloppy_imports_resolve; use deno_runtime::deno_node; use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::jsr::JsrDepPackageReq; @@ -62,7 +61,6 @@ use crate::npm::CliNpmResolver; use crate::resolver::CliCjsTracker; use crate::resolver::CliNpmGraphResolver; use crate::resolver::CliResolver; -use crate::resolver::CliSloppyImportsResolver; use crate::sys::CliSys; use crate::tools::check; use crate::tools::check::CheckError; @@ -949,11 +947,14 @@ pub fn maybe_additional_sloppy_imports_message( sys: &CliSys, specifier: &ModuleSpecifier, ) -> Option { + let (resolved, sloppy_reason) = sloppy_imports_resolve( + specifier, + deno_resolver::workspace::ResolutionKind::Execution, + sys.clone(), + )?; Some(format!( "{} {}", - CliSloppyImportsResolver::new(SloppyImportsCachedFs::new(sys.clone())) - .resolve(specifier, SloppyImportsResolutionKind::Execution)? - .as_suggestion_message(), + sloppy_reason.suggestion_message_for_specifier(&resolved), RUN_WITH_SLOPPY_IMPORTS_MSG )) } diff --git a/cli/lib/standalone/binary.rs b/cli/lib/standalone/binary.rs index ae02197bf47a4a..516d120a2001a2 100644 --- a/cli/lib/standalone/binary.rs +++ b/cli/lib/standalone/binary.rs @@ -3,8 +3,8 @@ use std::borrow::Cow; use std::collections::BTreeMap; -use deno_config::workspace::PackageJsonDepResolution; use deno_media_type::MediaType; +use deno_resolver::workspace::PackageJsonDepResolution; use deno_runtime::deno_permissions::PermissionsOptions; use deno_runtime::deno_telemetry::OtelConfig; use deno_semver::Version; diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 16a815e08ff7bb..6d62a68febe9c1 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -9,7 +9,6 @@ use std::path::Path; use deno_ast::SourceRange; use deno_ast::SourceRangedForSpanned; use deno_ast::SourceTextInfo; -use deno_config::workspace::MappedResolution; use deno_core::error::AnyError; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; @@ -20,6 +19,7 @@ use deno_error::JsErrorBox; use deno_lint::diagnostic::LintDiagnosticRange; use deno_path_util::url_to_file_path; use deno_resolver::npm::managed::NpmResolutionCell; +use deno_resolver::workspace::MappedResolution; use deno_runtime::deno_node::PathClean; use deno_semver::jsr::JsrPackageNvReference; use deno_semver::jsr::JsrPackageReqReference; @@ -1348,11 +1348,10 @@ impl CodeActionCollection { let npm_ref = if let Ok(resolution) = workspace_resolver.resolve( &dep_key, document.specifier(), - deno_config::workspace::ResolutionKind::Execution, + deno_resolver::workspace::ResolutionKind::Execution, ) { let specifier = match resolution { - MappedResolution::Normal { specifier, .. } - | MappedResolution::ImportMap { specifier, .. } => specifier, + MappedResolution::Normal { specifier, .. } => specifier, _ => { return None; } diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index b76c54c7ed6b5f..2e02d839e4ad19 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -21,16 +21,12 @@ use deno_config::deno_json::TsConfig; use deno_config::deno_json::TsConfigWithIgnoredOptions; use deno_config::glob::FilePatterns; use deno_config::glob::PathOrPatternSet; -use deno_config::workspace::CreateResolverOptions; -use deno_config::workspace::PackageJsonDepResolution; -use deno_config::workspace::SpecifiedImportMap; use deno_config::workspace::VendorEnablement; use deno_config::workspace::Workspace; use deno_config::workspace::WorkspaceCache; use deno_config::workspace::WorkspaceDirectory; use deno_config::workspace::WorkspaceDirectoryEmptyOptions; use deno_config::workspace::WorkspaceDiscoverOptions; -use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; @@ -49,7 +45,12 @@ use deno_npm::npm_rc::ResolvedNpmRc; use deno_package_json::PackageJsonCache; use deno_path_util::url_to_file_path; use deno_resolver::npmrc::discover_npmrc_from_workspace; -use deno_resolver::sloppy_imports::SloppyImportsCachedFs; +use deno_resolver::workspace::CreateResolverOptions; +use deno_resolver::workspace::FsCacheOptions; +use deno_resolver::workspace::PackageJsonDepResolution; +use deno_resolver::workspace::SloppyImportsOptions; +use deno_resolver::workspace::SpecifiedImportMap; +use deno_resolver::workspace::WorkspaceResolver; use deno_runtime::deno_node::PackageJson; use indexmap::IndexSet; use lsp_types::ClientCapabilities; @@ -65,7 +66,6 @@ use crate::args::LintFlags; use crate::args::LintOptions; use crate::file_fetcher::CliFileFetcher; use crate::lsp::logging::lsp_warn; -use crate::resolver::CliSloppyImportsResolver; use crate::sys::CliSys; use crate::tools::lint::CliLinter; use crate::tools::lint::CliLinterOptions; @@ -1206,7 +1206,6 @@ pub struct ConfigData { pub lockfile: Option>, pub npmrc: Option>, pub resolver: Arc>, - pub sloppy_imports_resolver: Option>, pub import_map_from_settings: Option, pub unstable: BTreeSet, watched_files: HashMap, @@ -1569,35 +1568,52 @@ impl ConfigData { None } }; - let resolver = member_dir + let unstable = member_dir .workspace - .create_resolver( - CliSys::default(), - CreateResolverOptions { - pkg_json_dep_resolution, - specified_import_map, + .unstable_features() + .iter() + .chain(settings.unstable.as_deref()) + .cloned() + .collect::>(); + let unstable_sloppy_imports = std::env::var("DENO_UNSTABLE_SLOPPY_IMPORTS") + .is_ok() + || unstable.contains("sloppy-imports"); + let resolver = WorkspaceResolver::from_workspace( + &member_dir.workspace, + CliSys::default(), + CreateResolverOptions { + pkg_json_dep_resolution, + specified_import_map, + sloppy_imports_options: if unstable_sloppy_imports { + SloppyImportsOptions::Enabled + } else { + SloppyImportsOptions::Disabled }, + fs_cache_options: FsCacheOptions::Disabled, + }, + ) + .inspect_err(|err| { + lsp_warn!( + " Failed to load resolver: {}", + err // will contain the specifier + ); + }) + .ok() + .unwrap_or_else(|| { + // create a dummy resolver + WorkspaceResolver::new_raw( + scope.clone(), + None, + member_dir.workspace.resolver_jsr_pkgs().collect(), + member_dir.workspace.package_jsons().cloned().collect(), + pkg_json_dep_resolution, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + CliSys::default(), ) - .inspect_err(|err| { - lsp_warn!( - " Failed to load resolver: {}", - err // will contain the specifier - ); - }) - .ok() - .unwrap_or_else(|| { - // create a dummy resolver - WorkspaceResolver::new_raw( - scope.clone(), - None, - member_dir.workspace.resolver_jsr_pkgs().collect(), - member_dir.workspace.package_jsons().cloned().collect(), - pkg_json_dep_resolution, - Default::default(), - Default::default(), - CliSys::default(), - ) - }); + }); if !resolver.diagnostics().is_empty() { lsp_warn!( " Resolver diagnostics:\n{}", @@ -1609,26 +1625,8 @@ impl ConfigData { .join("\n") ); } - let unstable = member_dir - .workspace - .unstable_features() - .iter() - .chain(settings.unstable.as_deref()) - .cloned() - .collect::>(); - let unstable_sloppy_imports = std::env::var("DENO_UNSTABLE_SLOPPY_IMPORTS") - .is_ok() - || unstable.contains("sloppy-imports"); - let sloppy_imports_resolver = unstable_sloppy_imports.then(|| { - Arc::new(CliSloppyImportsResolver::new( - SloppyImportsCachedFs::new_without_stat_cache(CliSys::default()), - )) - }); let resolver = Arc::new(resolver); - let lint_rule_provider = LintRuleProvider::new( - sloppy_imports_resolver.clone(), - Some(resolver.clone()), - ); + let lint_rule_provider = LintRuleProvider::new(Some(resolver.clone())); let lint_options = LintOptions::resolve( member_dir.dir_path(), @@ -1676,7 +1674,6 @@ impl ConfigData { canonicalized_scope, member_dir, resolver, - sloppy_imports_resolver, fmt_config, lint_config, test_config, diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 7283f2cf8f684a..3adbefd88843ab 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -27,9 +27,7 @@ use deno_graph::Resolution; use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_lint::linter::LintConfig as DenoLintConfig; -use deno_resolver::sloppy_imports::SloppyImportsCachedFs; -use deno_resolver::sloppy_imports::SloppyImportsResolution; -use deno_resolver::sloppy_imports::SloppyImportsResolutionKind; +use deno_resolver::workspace::sloppy_imports_resolve; use deno_runtime::deno_node; use deno_runtime::tokio_util::create_basic_runtime; use deno_semver::jsr::JsrPackageReqReference; @@ -64,7 +62,6 @@ use crate::graph_util; use crate::graph_util::enhanced_resolution_error_message; use crate::lsp::logging::lsp_warn; use crate::lsp::lsp_custom::DiagnosticBatchNotificationParams; -use crate::resolver::CliSloppyImportsResolver; use crate::sys::CliSys; use crate::tools::lint::CliLinter; use crate::tools::lint::CliLinterOptions; @@ -1013,7 +1010,7 @@ fn generate_lint_diagnostics( Arc::new(LintConfig::new_with_base(PathBuf::from("/"))), Arc::new(CliLinter::new(CliLinterOptions { configured_rules: { - let lint_rule_provider = LintRuleProvider::new(None, None); + let lint_rule_provider = LintRuleProvider::new(None); lint_rule_provider.resolve_lint_rules(Default::default(), None) }, fix: false, @@ -1443,14 +1440,14 @@ impl DenoDiagnostic { pub fn to_lsp_diagnostic(&self, range: &lsp::Range) -> lsp::Diagnostic { fn no_local_message( specifier: &ModuleSpecifier, - maybe_sloppy_resolution: Option<&SloppyImportsResolution>, + suggestion_message: Option, ) -> String { let mut message = format!( "Unable to load a local module: {}\n", to_percent_decoded_str(specifier.as_ref()) ); - if let Some(res) = maybe_sloppy_resolution { - message.push_str(&res.as_suggestion_message()); + if let Some(suggestion_message) = suggestion_message { + message.push_str(&suggestion_message); message.push('.'); } else { message.push_str("Please check the file path."); @@ -1467,17 +1464,15 @@ impl DenoDiagnostic { Self::NotInstalledJsr(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("JSR package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))), Self::NotInstalledNpm(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("npm package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))), Self::NoLocal(specifier) => { - let maybe_sloppy_resolution = CliSloppyImportsResolver::new( - SloppyImportsCachedFs::new(CliSys::default()) - ).resolve(specifier, SloppyImportsResolutionKind::Execution); - let data = maybe_sloppy_resolution.as_ref().map(|res| { + let sloppy_resolution = sloppy_imports_resolve(specifier, deno_resolver::workspace::ResolutionKind::Execution, CliSys::default()); + let data = sloppy_resolution.as_ref().map(|(resolved, sloppy_reason)| { json!({ "specifier": specifier, - "to": res.as_specifier(), - "message": res.as_quick_fix_message(), + "to": resolved, + "message": sloppy_reason.quick_fix_message_for_specifier(resolved), }) }); - (lsp::DiagnosticSeverity::ERROR, no_local_message(specifier, maybe_sloppy_resolution.as_ref()), data) + (lsp::DiagnosticSeverity::ERROR, no_local_message(specifier, sloppy_resolution.as_ref().map(|(resolved, sloppy_reason)| sloppy_reason.suggestion_message_for_specifier(resolved))), data) }, Self::Redirect { from, to} => (lsp::DiagnosticSeverity::INFORMATION, format!("The import of \"{from}\" was redirected to \"{to}\"."), Some(json!({ "specifier": from, "redirect": to }))), Self::ResolutionError(err) => { diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 81673b693b8a20..074372aab32994 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -12,8 +12,6 @@ use deno_ast::MediaType; use deno_cache_dir::npm::NpmCacheDir; use deno_cache_dir::HttpCache; use deno_config::deno_json::JsxImportSourceConfig; -use deno_config::workspace::PackageJsonDepResolution; -use deno_config::workspace::WorkspaceResolver; use deno_core::parking_lot::Mutex; use deno_core::url::Url; use deno_graph::GraphImport; @@ -29,6 +27,8 @@ use deno_resolver::npm::CreateInNpmPkgCheckerOptions; use deno_resolver::npm::DenoInNpmPackageChecker; use deno_resolver::npm::NpmReqResolverOptions; use deno_resolver::npmrc::create_default_npmrc; +use deno_resolver::workspace::PackageJsonDepResolution; +use deno_resolver::workspace::WorkspaceResolver; use deno_resolver::DenoResolverOptions; use deno_resolver::NodeAndNpmReqResolver; use deno_semver::jsr::JsrPackageReqReference; @@ -844,9 +844,6 @@ impl<'a> ResolverFactory<'a> { } _ => 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()) @@ -860,6 +857,8 @@ impl<'a> ResolverFactory<'a> { PackageJsonDepResolution::Disabled, Default::default(), Default::default(), + Default::default(), + Default::default(), self.sys.clone(), )) }), diff --git a/cli/resolver.rs b/cli/resolver.rs index 5dcdadb7ff579d..b9ca0eb3069dd8 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -4,8 +4,6 @@ use std::sync::Arc; use async_trait::async_trait; use dashmap::DashSet; -use deno_config::workspace::MappedResolutionDiagnostic; -use deno_config::workspace::MappedResolutionError; use deno_core::ModuleSpecifier; use deno_error::JsErrorBox; use deno_graph::source::ResolveError; @@ -14,8 +12,8 @@ use deno_graph::NpmLoadError; use deno_graph::NpmResolvePkgReqsResult; use deno_npm::resolution::NpmResolutionError; use deno_resolver::npm::DenoInNpmPackageChecker; -use deno_resolver::sloppy_imports::SloppyImportsCachedFs; -use deno_resolver::sloppy_imports::SloppyImportsResolver; +use deno_resolver::workspace::MappedResolutionDiagnostic; +use deno_resolver::workspace::MappedResolutionError; use deno_runtime::colors; use deno_runtime::deno_node::is_builtin_node_module; use deno_semver::package::PackageReq; @@ -35,14 +33,10 @@ pub type CliCjsTracker = deno_resolver::cjs::CjsTracker; pub type CliIsCjsResolver = deno_resolver::cjs::IsCjsResolver; -pub type CliSloppyImportsCachedFs = SloppyImportsCachedFs; -pub type CliSloppyImportsResolver = - SloppyImportsResolver; pub type CliDenoResolver = deno_resolver::DenoResolver< DenoInNpmPackageChecker, DenoIsBuiltInNodeModuleChecker, CliNpmResolver, - CliSloppyImportsCachedFs, CliSys, >; pub type CliNpmReqResolver = deno_resolver::npm::NpmReqResolver< diff --git a/cli/rt/run.rs b/cli/rt/run.rs index 3087682ef9fc06..1eca838cba643b 100644 --- a/cli/rt/run.rs +++ b/cli/rt/run.rs @@ -7,9 +7,7 @@ use std::sync::Arc; use std::sync::OnceLock; use deno_cache_dir::npm::NpmCacheDir; -use deno_config::workspace::MappedResolution; use deno_config::workspace::ResolverWorkspaceJsrPackage; -use deno_config::workspace::WorkspaceResolver; use deno_core::error::AnyError; use deno_core::error::ModuleLoaderError; use deno_core::futures::future::LocalBoxFuture; @@ -59,9 +57,9 @@ use deno_resolver::npm::NpmReqResolver; use deno_resolver::npm::NpmReqResolverOptions; use deno_resolver::npm::NpmResolver; use deno_resolver::npm::NpmResolverCreateOptions; -use deno_resolver::sloppy_imports::SloppyImportsCachedFs; -use deno_resolver::sloppy_imports::SloppyImportsResolutionKind; -use deno_resolver::sloppy_imports::SloppyImportsResolver; +use deno_resolver::workspace::MappedResolution; +use deno_resolver::workspace::SloppyImportsOptions; +use deno_resolver::workspace::WorkspaceResolver; use deno_runtime::code_cache::CodeCache; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::create_host_defined_options; @@ -107,8 +105,6 @@ struct SharedModuleLoaderState { npm_module_loader: Arc, npm_registry_permission_checker: NpmRegistryReadPermissionChecker, npm_req_resolver: Arc, - sloppy_imports_resolver: - Option>>, vfs: Arc, workspace_resolver: WorkspaceResolver, } @@ -210,7 +206,7 @@ impl ModuleLoader for EmbeddedModuleLoader { let mapped_resolution = self.shared.workspace_resolver.resolve( raw_specifier, &referrer, - deno_config::workspace::ResolutionKind::Execution, + deno_resolver::workspace::ResolutionKind::Execution, ); match mapped_resolution { @@ -289,8 +285,7 @@ impl ModuleLoader for EmbeddedModuleLoader { ) } }, - Ok(MappedResolution::Normal { specifier, .. }) - | Ok(MappedResolution::ImportMap { specifier, .. }) => { + Ok(MappedResolution::Normal { specifier, .. }) => { if let Ok(reference) = NpmPackageReqReference::from_specifier(&specifier) { @@ -322,18 +317,6 @@ impl ModuleLoader for EmbeddedModuleLoader { } } - // do sloppy imports resolution if enabled - let specifier = if let Some(sloppy_imports_resolver) = - &self.shared.sloppy_imports_resolver - { - sloppy_imports_resolver - .resolve(&specifier, SloppyImportsResolutionKind::Execution) - .map(|s| s.into_specifier()) - .unwrap_or(specifier) - } else { - specifier - }; - Ok( self .shared @@ -832,10 +815,6 @@ pub async fn run( pkg_json_resolver.clone(), sys.clone(), )); - let sloppy_imports_resolver = - metadata.unstable_config.sloppy_imports.then(|| { - SloppyImportsResolver::new(SloppyImportsCachedFs::new(sys.clone())) - }); let workspace_resolver = { let import_map = match metadata.workspace_resolver.import_map { Some(import_map) => Some( @@ -883,6 +862,12 @@ pub async fn run( .collect(), pkg_jsons, metadata.workspace_resolver.pkg_json_resolution, + if metadata.unstable_config.sloppy_imports { + SloppyImportsOptions::Enabled + } else { + SloppyImportsOptions::Disabled + }, + Default::default(), Default::default(), Default::default(), sys.clone(), @@ -915,7 +900,6 @@ pub async fn run( )), npm_registry_permission_checker, npm_req_resolver, - sloppy_imports_resolver, vfs: vfs.clone(), workspace_resolver, }), diff --git a/cli/standalone/binary.rs b/cli/standalone/binary.rs index 4f0ce5a3c00f63..ccd0b641852108 100644 --- a/cli/standalone/binary.rs +++ b/cli/standalone/binary.rs @@ -16,7 +16,6 @@ use capacity_builder::BytesAppendable; use deno_ast::MediaType; use deno_ast::ModuleKind; use deno_ast::ModuleSpecifier; -use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; @@ -49,6 +48,7 @@ use deno_npm::resolution::SerializedNpmResolutionSnapshot; use deno_npm::NpmSystemInfo; use deno_path_util::url_from_directory_path; use deno_path_util::url_to_file_path; +use deno_resolver::workspace::WorkspaceResolver; use indexmap::IndexMap; use node_resolver::analyze::CjsAnalysis; use node_resolver::analyze::CjsCodeAnalyzer; diff --git a/cli/tools/info.rs b/cli/tools/info.rs index e6bf1baa36b5d2..f7368f90af224c 100644 --- a/cli/tools/info.rs +++ b/cli/tools/info.rs @@ -59,21 +59,18 @@ pub async fn info( let maybe_import_specifier = if let Ok(resolved) = resolver.resolve( &specifier, &cwd_url, - deno_config::workspace::ResolutionKind::Execution, + deno_resolver::workspace::ResolutionKind::Execution, ) { match resolved { - deno_config::workspace::MappedResolution::Normal { - specifier, .. - } - | deno_config::workspace::MappedResolution::ImportMap { + deno_resolver::workspace::MappedResolution::Normal { specifier, .. } - | deno_config::workspace::MappedResolution::WorkspaceJsrPackage { + | deno_resolver::workspace::MappedResolution::WorkspaceJsrPackage { specifier, .. } => Some(specifier), - deno_config::workspace::MappedResolution::WorkspaceNpmPackage { + deno_resolver::workspace::MappedResolution::WorkspaceNpmPackage { target_pkg_json, sub_path, .. @@ -88,7 +85,7 @@ pub async fn info( )? .into_url()?, ), - deno_config::workspace::MappedResolution::PackageJson { + deno_resolver::workspace::MappedResolution::PackageJson { alias, sub_path, dep_result, diff --git a/cli/tools/lint/mod.rs b/cli/tools/lint/mod.rs index 74c46d4c189112..04734151a67839 100644 --- a/cli/tools/lint/mod.rs +++ b/cli/tools/lint/mod.rs @@ -499,7 +499,7 @@ fn collect_lint_files( #[allow(clippy::print_stdout)] pub fn print_rules_list(json: bool, maybe_rules_tags: Option>) { - let rule_provider = LintRuleProvider::new(None, None); + let rule_provider = LintRuleProvider::new(None); let mut all_rules = rule_provider.all_rules(); let configured_rules = rule_provider.resolve_lint_rules( LintRulesConfig { @@ -686,7 +686,7 @@ mod tests { } fn get_all_rules() -> Vec { - let rule_provider = LintRuleProvider::new(None, None); + let rule_provider = LintRuleProvider::new(None); let configured_rules = rule_provider.resolve_lint_rules(Default::default(), None); let mut all_rules = configured_rules diff --git a/cli/tools/lint/rules/mod.rs b/cli/tools/lint/rules/mod.rs index 9f2cee24fa9b21..99ce3331d3ec78 100644 --- a/cli/tools/lint/rules/mod.rs +++ b/cli/tools/lint/rules/mod.rs @@ -7,15 +7,14 @@ use std::sync::Arc; use deno_ast::ModuleSpecifier; use deno_config::deno_json::ConfigFile; use deno_config::deno_json::LintRulesConfig; -use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_graph::ModuleGraph; use deno_lint::diagnostic::LintDiagnostic; use deno_lint::rules::LintRule; use deno_lint::tags; +use deno_resolver::workspace::WorkspaceResolver; -use crate::resolver::CliSloppyImportsResolver; use crate::sys::CliSys; mod no_sloppy_imports; @@ -141,19 +140,14 @@ impl ConfiguredRules { } pub struct LintRuleProvider { - sloppy_imports_resolver: Option>, workspace_resolver: Option>>, } impl LintRuleProvider { pub fn new( - sloppy_imports_resolver: Option>, workspace_resolver: Option>>, ) -> Self { - Self { - sloppy_imports_resolver, - workspace_resolver, - } + Self { workspace_resolver } } pub fn resolve_lint_rules_err_empty( @@ -172,7 +166,6 @@ impl LintRuleProvider { let deno_lint_rules = deno_lint::rules::get_all_rules(); let cli_lint_rules = vec![CliLintRule(CliLintRuleKind::Extended( Box::new(no_sloppy_imports::NoSloppyImportsRule::new( - self.sloppy_imports_resolver.clone(), self.workspace_resolver.clone(), )), ))]; @@ -274,7 +267,7 @@ mod test { include: None, tags: None, }; - let rules_provider = LintRuleProvider::new(None, None); + let rules_provider = LintRuleProvider::new(None); let rules = rules_provider.resolve_lint_rules(rules_config, None); let mut rule_names = rules .rules diff --git a/cli/tools/lint/rules/no_sloppy_imports.rs b/cli/tools/lint/rules/no_sloppy_imports.rs index 34eeef521d95ae..48b0e5015f8807 100644 --- a/cli/tools/lint/rules/no_sloppy_imports.rs +++ b/cli/tools/lint/rules/no_sloppy_imports.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use std::sync::Arc; use deno_ast::SourceRange; -use deno_config::workspace::WorkspaceResolver; use deno_error::JsErrorBox; use deno_graph::source::ResolutionKind; use deno_graph::source::ResolveError; @@ -17,31 +16,25 @@ use deno_lint::diagnostic::LintFix; use deno_lint::diagnostic::LintFixChange; use deno_lint::rules::LintRule; use deno_lint::tags; -use deno_resolver::sloppy_imports::SloppyImportsResolution; -use deno_resolver::sloppy_imports::SloppyImportsResolutionKind; +use deno_resolver::workspace::SloppyImportsResolutionReason; +use deno_resolver::workspace::WorkspaceResolver; use text_lines::LineAndColumnIndex; use super::ExtendedLintRule; use crate::graph_util::CliJsrUrlProvider; -use crate::resolver::CliSloppyImportsResolver; use crate::sys::CliSys; #[derive(Debug)] pub struct NoSloppyImportsRule { - sloppy_imports_resolver: Option>, // None for making printing out the lint rules easy workspace_resolver: Option>>, } impl NoSloppyImportsRule { pub fn new( - sloppy_imports_resolver: Option>, workspace_resolver: Option>>, ) -> Self { - NoSloppyImportsRule { - sloppy_imports_resolver, - workspace_resolver, - } + NoSloppyImportsRule { workspace_resolver } } } @@ -54,7 +47,11 @@ impl ExtendedLintRule for NoSloppyImportsRule { // do sloppy import resolution because sloppy import // resolution requires knowing about the surrounding files // in addition to the current one - self.sloppy_imports_resolver.is_none() || self.workspace_resolver.is_none() + let Some(workspace_resolver) = &self.workspace_resolver else { + return true; + }; + !workspace_resolver.sloppy_imports_enabled() + && !workspace_resolver.has_compiler_options_root_dirs() } fn help_docs_url(&self) -> Cow<'static, str> { @@ -75,16 +72,12 @@ impl LintRule for NoSloppyImportsRule { let Some(workspace_resolver) = &self.workspace_resolver else { return; }; - let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver else { - return; - }; if context.specifier().scheme() != "file" { return; } let resolver = SloppyImportCaptureResolver { workspace_resolver, - sloppy_imports_resolver, captures: Default::default(), }; @@ -102,7 +95,9 @@ impl LintRule for NoSloppyImportsRule { maybe_npm_resolver: None, }); - for (referrer, sloppy_import) in resolver.captures.borrow_mut().drain() { + for (referrer, (specifier, sloppy_reason)) in + resolver.captures.borrow_mut().drain() + { let start_range = context.text_info().loc_to_source_pos(LineAndColumnIndex { line_index: referrer.range.start.line, @@ -126,10 +121,12 @@ impl LintRule for NoSloppyImportsRule { custom_docs_url: Some(DOCS_URL.to_string()), fixes: context .specifier() - .make_relative(sloppy_import.as_specifier()) + .make_relative(&specifier) .map(|relative| { vec![LintFix { - description: Cow::Owned(sloppy_import.as_quick_fix_message()), + description: Cow::Owned( + sloppy_reason.quick_fix_message_for_specifier(&specifier), + ), changes: vec![LintFixChange { new_text: Cow::Owned({ let relative = if relative.starts_with("../") { @@ -176,8 +173,9 @@ impl LintRule for NoSloppyImportsRule { #[derive(Debug)] struct SloppyImportCaptureResolver<'a> { workspace_resolver: &'a WorkspaceResolver, - sloppy_imports_resolver: &'a CliSloppyImportsResolver, - captures: RefCell>, + captures: RefCell< + HashMap, + >, } impl<'a> deno_graph::source::Resolver for SloppyImportCaptureResolver<'a> { @@ -194,45 +192,37 @@ impl<'a> deno_graph::source::Resolver for SloppyImportCaptureResolver<'a> { &referrer_range.specifier, match resolution_kind { ResolutionKind::Execution => { - deno_config::workspace::ResolutionKind::Execution + deno_resolver::workspace::ResolutionKind::Execution } ResolutionKind::Types => { - deno_config::workspace::ResolutionKind::Types + deno_resolver::workspace::ResolutionKind::Types } }, ) .map_err(|err| ResolveError::Other(JsErrorBox::from_err(err)))?; match resolution { - deno_config::workspace::MappedResolution::Normal { - specifier, .. - } - | deno_config::workspace::MappedResolution::ImportMap { - specifier, .. - } => match self.sloppy_imports_resolver.resolve( - &specifier, - match resolution_kind { - ResolutionKind::Execution => SloppyImportsResolutionKind::Execution, - ResolutionKind::Types => SloppyImportsResolutionKind::Types, - }, - ) { - Some(res) => { + deno_resolver::workspace::MappedResolution::Normal { + specifier, + sloppy_reason, + .. + } => { + if let Some(sloppy_reason) = sloppy_reason { self .captures .borrow_mut() .entry(referrer_range.clone()) - .or_insert_with(|| res.clone()); - Ok(res.into_specifier()) + .or_insert_with(|| (specifier.clone(), sloppy_reason)); } - None => Ok(specifier), - }, - deno_config::workspace::MappedResolution::WorkspaceJsrPackage { + Ok(specifier) + } + deno_resolver::workspace::MappedResolution::WorkspaceJsrPackage { .. } - | deno_config::workspace::MappedResolution::WorkspaceNpmPackage { + | deno_resolver::workspace::MappedResolution::WorkspaceNpmPackage { .. } - | deno_config::workspace::MappedResolution::PackageJson { .. } => { + | deno_resolver::workspace::MappedResolution::PackageJson { .. } => { // this error is ignored Err(ResolveError::Other(JsErrorBox::generic(""))) } diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs index c2ed94e8473aee..1834774d0c4dad 100644 --- a/cli/tools/registry/mod.rs +++ b/cli/tools/registry/mod.rs @@ -120,7 +120,6 @@ pub async fn publish( } let specifier_unfurler = Arc::new(SpecifierUnfurler::new( - cli_factory.sloppy_imports_resolver()?.cloned(), cli_factory.workspace_resolver().await?.clone(), cli_options.unstable_bare_node_builtins(), )); diff --git a/cli/tools/registry/unfurl.rs b/cli/tools/registry/unfurl.rs index 469a19fdf4124f..526b4cbdcb71de 100644 --- a/cli/tools/registry/unfurl.rs +++ b/cli/tools/registry/unfurl.rs @@ -15,9 +15,6 @@ use deno_ast::ParsedSource; use deno_ast::SourceRange; use deno_ast::SourceTextInfo; use deno_ast::SourceTextProvider; -use deno_config::workspace::MappedResolution; -use deno_config::workspace::PackageJsonDepResolution; -use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow; use deno_core::ModuleSpecifier; use deno_graph::DependencyDescriptor; @@ -27,12 +24,13 @@ use deno_graph::StaticDependencyKind; use deno_graph::TypeScriptReference; use deno_package_json::PackageJsonDepValue; use deno_package_json::PackageJsonDepWorkspaceReq; -use deno_resolver::sloppy_imports::SloppyImportsResolutionKind; +use deno_resolver::workspace::MappedResolution; +use deno_resolver::workspace::PackageJsonDepResolution; +use deno_resolver::workspace::WorkspaceResolver; use deno_runtime::deno_node::is_builtin_node_module; use deno_semver::Version; use deno_semver::VersionReq; -use crate::resolver::CliSloppyImportsResolver; use crate::sys::CliSys; #[derive(Debug, Clone)] @@ -190,14 +188,12 @@ enum UnfurlSpecifierError { } pub struct SpecifierUnfurler { - sloppy_imports_resolver: Option>, workspace_resolver: Arc>, bare_node_builtins: bool, } impl SpecifierUnfurler { pub fn new( - sloppy_imports_resolver: Option>, workspace_resolver: Arc>, bare_node_builtins: bool, ) -> Self { @@ -206,7 +202,6 @@ impl SpecifierUnfurler { PackageJsonDepResolution::Enabled ); Self { - sloppy_imports_resolver, workspace_resolver, bare_node_builtins, } @@ -216,7 +211,7 @@ impl SpecifierUnfurler { &self, referrer: &ModuleSpecifier, specifier: &str, - resolution_kind: SloppyImportsResolutionKind, + resolution_kind: deno_resolver::workspace::ResolutionKind, text_info: &SourceTextInfo, range: &deno_graph::PositionRange, diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic), @@ -251,16 +246,15 @@ impl SpecifierUnfurler { &self, referrer: &ModuleSpecifier, specifier: &str, - resolution_kind: SloppyImportsResolutionKind, + resolution_kind: deno_resolver::workspace::ResolutionKind, ) -> Result, UnfurlSpecifierError> { - let resolved = if let Ok(resolved) = self.workspace_resolver.resolve( - specifier, - referrer, - resolution_kind.into(), - ) { + let resolved = if let Ok(resolved) = + self + .workspace_resolver + .resolve(specifier, referrer, resolution_kind) + { match resolved { - MappedResolution::Normal { specifier, .. } - | MappedResolution::ImportMap { specifier, .. } => Some(specifier), + MappedResolution::Normal { specifier, .. } => Some(specifier), MappedResolution::WorkspaceJsrPackage { pkg_req_ref, .. } => { Some(ModuleSpecifier::parse(&pkg_req_ref.to_string()).unwrap()) } @@ -398,15 +392,6 @@ impl SpecifierUnfurler { // } else { // resolved // }; - let resolved = - if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { - sloppy_imports_resolver - .resolve(&resolved, resolution_kind) - .map(|res| res.into_specifier()) - .unwrap_or(resolved) - } else { - resolved - }; let relative_resolved = relative_url(&resolved, referrer); if relative_resolved == specifier { Ok(None) // nothing to unfurl @@ -464,7 +449,7 @@ impl SpecifierUnfurler { let maybe_unfurled = self.unfurl_specifier_reporting_diagnostic( module_url, specifier, - SloppyImportsResolutionKind::Execution, // dynamic imports are always execution + deno_resolver::workspace::ResolutionKind::Execution, // dynamic imports are always execution text_info, &dep.argument_range, diagnostic_reporter, @@ -492,7 +477,7 @@ impl SpecifierUnfurler { let unfurled = self.unfurl_specifier_reporting_diagnostic( module_url, specifier, - SloppyImportsResolutionKind::Execution, // dynamic imports are always execution + deno_resolver::workspace::ResolutionKind::Execution, // dynamic imports are always execution text_info, &dep.argument_range, diagnostic_reporter, @@ -538,7 +523,7 @@ impl SpecifierUnfurler { let analyze_specifier = |specifier: &str, range: &deno_graph::PositionRange, - resolution_kind: SloppyImportsResolutionKind, + resolution_kind: deno_resolver::workspace::ResolutionKind, text_changes: &mut Vec, diagnostic_reporter: &mut dyn FnMut(SpecifierUnfurlerDiagnostic)| { if let Some(unfurled) = self.unfurl_specifier_reporting_diagnostic( @@ -559,18 +544,18 @@ impl SpecifierUnfurler { match dep { DependencyDescriptor::Static(dep) => { let resolution_kind = if parsed_source.media_type().is_declaration() { - SloppyImportsResolutionKind::Types + deno_resolver::workspace::ResolutionKind::Types } else { match dep.kind { StaticDependencyKind::Export | StaticDependencyKind::Import | StaticDependencyKind::ExportEquals | StaticDependencyKind::ImportEquals => { - SloppyImportsResolutionKind::Execution + deno_resolver::workspace::ResolutionKind::Execution } StaticDependencyKind::ExportType | StaticDependencyKind::ImportType => { - SloppyImportsResolutionKind::Types + deno_resolver::workspace::ResolutionKind::Types } } }; @@ -616,7 +601,7 @@ impl SpecifierUnfurler { analyze_specifier( &specifier_with_range.text, &specifier_with_range.range, - SloppyImportsResolutionKind::Types, + deno_resolver::workspace::ResolutionKind::Types, &mut text_changes, diagnostic_reporter, ); @@ -625,7 +610,7 @@ impl SpecifierUnfurler { analyze_specifier( &jsdoc.specifier.text, &jsdoc.specifier.range, - SloppyImportsResolutionKind::Types, + deno_resolver::workspace::ResolutionKind::Types, &mut text_changes, diagnostic_reporter, ); @@ -634,7 +619,7 @@ impl SpecifierUnfurler { analyze_specifier( &specifier_with_range.text, &specifier_with_range.range, - SloppyImportsResolutionKind::Execution, + deno_resolver::workspace::ResolutionKind::Execution, &mut text_changes, diagnostic_reporter, ); @@ -643,7 +628,7 @@ impl SpecifierUnfurler { analyze_specifier( &specifier_with_range.text, &specifier_with_range.range, - SloppyImportsResolutionKind::Types, + deno_resolver::workspace::ResolutionKind::Types, &mut text_changes, diagnostic_reporter, ); @@ -700,7 +685,7 @@ mod tests { use deno_config::workspace::ResolverWorkspaceJsrPackage; use deno_core::serde_json::json; use deno_core::url::Url; - use deno_resolver::sloppy_imports::SloppyImportsCachedFs; + use deno_resolver::workspace::SloppyImportsOptions; use deno_runtime::deno_node::PackageJson; use deno_semver::Version; use import_map::ImportMapWithDiagnostics; @@ -760,18 +745,14 @@ mod tests { exports: IndexMap::from([(".".to_string(), "mod.ts".to_string())]), }], vec![Arc::new(package_json)], - deno_config::workspace::PackageJsonDepResolution::Enabled, + deno_resolver::workspace::PackageJsonDepResolution::Enabled, + SloppyImportsOptions::Enabled, + Default::default(), Default::default(), Default::default(), CliSys::default(), ); - let unfurler = SpecifierUnfurler::new( - Some(Arc::new(CliSloppyImportsResolver::new( - SloppyImportsCachedFs::new(CliSys::default()), - ))), - Arc::new(workspace_resolver), - true, - ); + let unfurler = SpecifierUnfurler::new(Arc::new(workspace_resolver), true); // Unfurling TS file should apply changes. { @@ -926,18 +907,14 @@ export type * from "./c.d.ts"; Arc::new(pkg_json_subtract), Arc::new(pkg_json_publishing), ], - deno_config::workspace::PackageJsonDepResolution::Enabled, + deno_resolver::workspace::PackageJsonDepResolution::Enabled, + Default::default(), + Default::default(), Default::default(), Default::default(), sys.clone(), ); - let unfurler = SpecifierUnfurler::new( - Some(Arc::new(CliSloppyImportsResolver::new( - SloppyImportsCachedFs::new(sys), - ))), - Arc::new(workspace_resolver), - true, - ); + let unfurler = SpecifierUnfurler::new(Arc::new(workspace_resolver), true); { let source_code = r#"import add from "add"; diff --git a/resolvers/deno/Cargo.toml b/resolvers/deno/Cargo.toml index b333ba19c53970..92f509a4cff6a7 100644 --- a/resolvers/deno/Cargo.toml +++ b/resolvers/deno/Cargo.toml @@ -33,13 +33,18 @@ deno_path_util.workspace = true deno_semver.workspace = true deno_terminal.workspace = true futures.workspace = true +import_map.workspace = true +indexmap.workspace = true log.workspace = true node_resolver.workspace = true once_cell.workspace = true parking_lot.workspace = true +serde.workspace = true +serde_json.workspace = true sys_traits.workspace = true thiserror.workspace = true url.workspace = true [dev-dependencies] +sys_traits = { workspace = true, features = ["memory", "real", "serde_json"] } test_util.workspace = true diff --git a/resolvers/deno/factory.rs b/resolvers/deno/factory.rs index 57f78a546c021a..4e1a59788c6129 100644 --- a/resolvers/deno/factory.rs +++ b/resolvers/deno/factory.rs @@ -12,7 +12,6 @@ use deno_cache_dir::HttpCacheRc; use deno_cache_dir::LocalHttpCache; use deno_config::deno_json::NodeModulesDirMode; use deno_config::workspace::FolderConfigs; -use deno_config::workspace::PackageJsonDepResolution; use deno_config::workspace::VendorEnablement; use deno_config::workspace::WorkspaceDirectory; use deno_config::workspace::WorkspaceDirectoryEmptyOptions; @@ -61,12 +60,13 @@ use crate::npm::NpmResolverCreateOptions; use crate::npmrc::discover_npmrc_from_workspace; use crate::npmrc::NpmRcDiscoverError; use crate::npmrc::ResolvedNpmRcRc; -use crate::sloppy_imports::SloppyImportsCachedFs; -use crate::sloppy_imports::SloppyImportsResolver; -use crate::sloppy_imports::SloppyImportsResolverRc; use crate::sync::new_rc; use crate::sync::MaybeSend; use crate::sync::MaybeSync; +use crate::workspace::FsCacheOptions; +use crate::workspace::PackageJsonDepResolution; +use crate::workspace::SloppyImportsOptions; +use crate::workspace::WorkspaceResolver; use crate::DefaultDenoResolverRc; use crate::DenoResolver; use crate::DenoResolverOptions; @@ -133,7 +133,7 @@ pub trait SpecifiedImportMapProvider: { async fn get( &self, - ) -> Result, anyhow::Error>; + ) -> Result, anyhow::Error>; } #[derive(Debug, Clone)] @@ -560,7 +560,6 @@ impl WorkspaceFactory { #[derive(Debug, Default)] pub struct ResolverFactoryOptions { pub conditions_from_resolution_mode: ConditionsFromResolutionMode, - pub no_sloppy_imports_cache: bool, pub npm_system_info: NpmSystemInfo, pub node_resolution_cache: Option, pub package_json_cache: Option, @@ -593,8 +592,6 @@ pub struct ResolverFactory { npm_resolver: Deferred>, npm_resolution: NpmResolutionCellRc, pkg_json_resolver: Deferred>, - sloppy_imports_resolver: - Deferred>>>, workspace_factory: WorkspaceFactoryRc, workspace_resolver: async_once_cell::OnceCell>, } @@ -616,7 +613,6 @@ impl ResolverFactory { npm_resolution: Default::default(), npm_resolver: Default::default(), pkg_json_resolver: Default::default(), - sloppy_imports_resolver: Default::default(), workspace_factory, workspace_resolver: Default::default(), options, @@ -646,7 +642,6 @@ impl ResolverFactory { .workspace_directory()? .workspace .vendor_dir_path(), - sloppy_imports_resolver: self.sloppy_imports_resolver()?.cloned(), workspace_resolver: self.workspace_resolver().await?.clone(), }))) } @@ -770,38 +765,6 @@ impl ResolverFactory { }) } - pub fn sloppy_imports_resolver( - &self, - ) -> Result< - Option<&SloppyImportsResolverRc>>, - anyhow::Error, - > { - self - .sloppy_imports_resolver - .get_or_try_init(|| { - let enabled = self.options.unstable_sloppy_imports - || self - .workspace_factory - .workspace_directory()? - .workspace - .has_unstable("sloppy-imports"); - if enabled { - Ok(Some(new_rc(SloppyImportsResolver::new( - if self.options.no_sloppy_imports_cache { - SloppyImportsCachedFs::new_without_stat_cache( - self.workspace_factory.sys.clone(), - ) - } else { - SloppyImportsCachedFs::new(self.workspace_factory.sys.clone()) - }, - )))) - } else { - Ok(None) - } - }) - .map(|v| v.as_ref()) - } - pub async fn workspace_resolver( &self, ) -> Result<&WorkspaceResolverRc, anyhow::Error> { @@ -815,7 +778,7 @@ impl ResolverFactory { Some(import_map) => import_map.get().await?, None => None, }; - let options = deno_config::workspace::CreateResolverOptions { + let options = crate::workspace::CreateResolverOptions { pkg_json_dep_resolution: match self .options .package_json_dep_resolution @@ -834,9 +797,24 @@ impl ResolverFactory { } }, specified_import_map, + sloppy_imports_options: if self.options.unstable_sloppy_imports + || self + .workspace_factory + .workspace_directory()? + .workspace + .has_unstable("sloppy-imports") + { + SloppyImportsOptions::Enabled + } else { + SloppyImportsOptions::Disabled + }, + fs_cache_options: FsCacheOptions::Enabled, }; - let resolver = workspace - .create_resolver(self.workspace_factory.sys.clone(), options)?; + let resolver = WorkspaceResolver::from_workspace( + workspace, + self.workspace_factory.sys.clone(), + options, + )?; if !resolver.diagnostics().is_empty() { // todo(dsherret): do not log this in this crate... that should be // a CLI responsibility diff --git a/resolvers/deno/lib.rs b/resolvers/deno/lib.rs index 26272af3bcb1ca..4ecc222ba6f8d7 100644 --- a/resolvers/deno/lib.rs +++ b/resolvers/deno/lib.rs @@ -7,11 +7,6 @@ use std::path::PathBuf; use boxed_error::Boxed; use deno_cache_dir::npm::NpmCacheDir; -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_error::JsError; use deno_package_json::PackageJsonDepValue; use deno_package_json::PackageJsonDepValueParseError; @@ -31,9 +26,6 @@ use npm::NpmReqResolverRc; use npm::ResolveIfForNpmPackageErrorKind; use npm::ResolvePkgFolderFromDenoReqError; use npm::ResolveReqWithSubPathErrorKind; -use sloppy_imports::SloppyImportResolverFs; -use sloppy_imports::SloppyImportsResolutionKind; -use sloppy_imports::SloppyImportsResolverRc; use sys_traits::FsCanonicalize; use sys_traits::FsMetadata; use sys_traits::FsRead; @@ -41,12 +33,18 @@ use sys_traits::FsReadDir; use thiserror::Error; use url::Url; +use crate::workspace::MappedResolution; +use crate::workspace::MappedResolutionDiagnostic; +use crate::workspace::MappedResolutionError; +use crate::workspace::WorkspaceResolvePkgJsonFolderError; +use crate::workspace::WorkspaceResolver; + pub mod cjs; pub mod factory; pub mod npm; pub mod npmrc; -pub mod sloppy_imports; mod sync; +pub mod workspace; #[allow(clippy::disallowed_types)] pub type WorkspaceResolverRc = @@ -128,7 +126,6 @@ pub struct DenoResolverOptions< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, - TSloppyImportResolverFs: SloppyImportResolverFs, TSys: FsCanonicalize + FsMetadata + FsRead + FsReadDir, > { pub in_npm_pkg_checker: TInNpmPackageChecker, @@ -140,8 +137,6 @@ pub struct DenoResolverOptions< TSys, >, >, - pub sloppy_imports_resolver: - Option>, pub workspace_resolver: WorkspaceResolverRc, /// Whether "bring your own node_modules" is enabled where Deno does not /// setup the node_modules directories automatically, but instead uses @@ -155,14 +150,12 @@ pub type DenoResolverRc< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, - TSloppyImportResolverFs, TSys, > = crate::sync::MaybeArc< DenoResolver< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, - TSloppyImportResolverFs, TSys, >, >; @@ -173,7 +166,6 @@ pub type DefaultDenoResolverRc = DenoResolverRc< npm::DenoInNpmPackageChecker, node_resolver::DenoIsBuiltInNodeModuleChecker, npm::NpmResolver, - sloppy_imports::SloppyImportsCachedFs, TSys, >; @@ -184,7 +176,6 @@ pub struct DenoResolver< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, - TSloppyImportResolverFs: SloppyImportResolverFs, TSys: FsCanonicalize + FsMetadata + FsRead + FsReadDir, > { in_npm_pkg_checker: TInNpmPackageChecker, @@ -196,8 +187,6 @@ pub struct DenoResolver< TSys, >, >, - sloppy_imports_resolver: - Option>, workspace_resolver: WorkspaceResolverRc, is_byonm: bool, maybe_vendor_specifier: Option, @@ -207,14 +196,12 @@ impl< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, - TSloppyImportResolverFs: SloppyImportResolverFs, TSys: FsCanonicalize + FsMetadata + FsRead + FsReadDir, > DenoResolver< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, - TSloppyImportResolverFs, TSys, > { @@ -223,14 +210,12 @@ impl< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, - TSloppyImportResolverFs, TSys, >, ) -> 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 @@ -277,33 +262,10 @@ impl< 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 resolution_kind { - NodeResolutionKind::Execution => { - SloppyImportsResolutionKind::Execution - } - NodeResolutionKind::Types => { - SloppyImportsResolutionKind::Types - } - }, - ) - .map(|s| s.into_specifier()) - .unwrap_or(specifier), - ) - } else { - Ok(specifier) - } + Ok(specifier) } MappedResolution::WorkspaceJsrPackage { specifier, .. } => { Ok(specifier) diff --git a/resolvers/deno/sloppy_imports.rs b/resolvers/deno/sloppy_imports.rs deleted file mode 100644 index 8a43be16aa3917..00000000000000 --- a/resolvers/deno/sloppy_imports.rs +++ /dev/null @@ -1,582 +0,0 @@ -// Copyright 2018-2025 the Deno authors. MIT license. - -use std::borrow::Cow; -use std::path::Path; -use std::path::PathBuf; - -use deno_media_type::MediaType; -use deno_path_util::url_from_file_path; -use deno_path_util::url_to_file_path; -use sys_traits::FsMetadata; -use sys_traits::FsMetadataValue; -use url::Url; - -use crate::sync::MaybeDashMap; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SloppyImportsFsEntry { - File, - Dir, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SloppyImportsResolution { - /// Ex. `./file.js` to `./file.ts` - JsToTs(Url), - /// Ex. `./file` to `./file.ts` - NoExtension(Url), - /// Ex. `./dir` to `./dir/index.ts` - Directory(Url), -} - -impl SloppyImportsResolution { - pub fn as_specifier(&self) -> &Url { - match self { - Self::JsToTs(specifier) => specifier, - Self::NoExtension(specifier) => specifier, - Self::Directory(specifier) => specifier, - } - } - - pub fn into_specifier(self) -> Url { - match self { - Self::JsToTs(specifier) => specifier, - Self::NoExtension(specifier) => specifier, - Self::Directory(specifier) => specifier, - } - } - - pub fn as_suggestion_message(&self) -> String { - format!("Maybe {}", self.as_base_message()) - } - - pub fn as_quick_fix_message(&self) -> String { - let message = self.as_base_message(); - let mut chars = message.chars(); - format!( - "{}{}.", - chars.next().unwrap().to_uppercase(), - chars.as_str() - ) - } - - fn as_base_message(&self) -> String { - match self { - SloppyImportsResolution::JsToTs(specifier) => { - let media_type = MediaType::from_specifier(specifier); - format!("change the extension to '{}'", media_type.as_ts_extension()) - } - SloppyImportsResolution::NoExtension(specifier) => { - let media_type = MediaType::from_specifier(specifier); - format!("add a '{}' extension", media_type.as_ts_extension()) - } - SloppyImportsResolution::Directory(specifier) => { - let file_name = specifier - .path() - .rsplit_once('/') - .map(|(_, file_name)| file_name) - .unwrap_or(specifier.path()); - format!("specify path to '{}' file in directory instead", file_name) - } - } - } -} - -/// The kind of resolution currently being done. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SloppyImportsResolutionKind { - /// Resolving for code that will be executed. - Execution, - /// Resolving for code that will be used for type information. - Types, -} - -impl SloppyImportsResolutionKind { - pub fn is_types(&self) -> bool { - *self == SloppyImportsResolutionKind::Types - } -} - -impl From - for deno_config::workspace::ResolutionKind -{ - fn from(value: SloppyImportsResolutionKind) -> Self { - match value { - SloppyImportsResolutionKind::Execution => Self::Execution, - SloppyImportsResolutionKind::Types => Self::Types, - } - } -} - -pub trait SloppyImportResolverFs { - fn stat_sync(&self, path: &Path) -> Option; - - fn is_file(&self, path: &Path) -> bool { - self.stat_sync(path) == Some(SloppyImportsFsEntry::File) - } -} - -#[allow(clippy::disallowed_types)] -pub type SloppyImportsResolverRc = - crate::sync::MaybeArc>; - -#[derive(Debug)] -pub struct SloppyImportsResolver { - fs: Fs, -} - -impl SloppyImportsResolver { - pub fn new(fs: Fs) -> Self { - Self { fs } - } - - pub fn resolve( - &self, - specifier: &Url, - resolution_kind: SloppyImportsResolutionKind, - ) -> Option { - fn path_without_ext( - path: &Path, - media_type: MediaType, - ) -> Option> { - let old_path_str = path.to_string_lossy(); - match media_type { - MediaType::Unknown => Some(old_path_str), - _ => old_path_str - .strip_suffix(media_type.as_ts_extension()) - .map(|s| Cow::Owned(s.to_string())), - } - } - - fn media_types_to_paths( - path_no_ext: &str, - original_media_type: MediaType, - probe_media_type_types: Vec, - reason: SloppyImportsResolutionReason, - ) -> Vec<(PathBuf, SloppyImportsResolutionReason)> { - probe_media_type_types - .into_iter() - .filter(|media_type| *media_type != original_media_type) - .map(|media_type| { - ( - PathBuf::from(format!( - "{}{}", - path_no_ext, - media_type.as_ts_extension() - )), - reason, - ) - }) - .collect::>() - } - - if specifier.scheme() != "file" { - return None; - } - - let path = url_to_file_path(specifier).ok()?; - - #[derive(Clone, Copy)] - enum SloppyImportsResolutionReason { - JsToTs, - NoExtension, - Directory, - } - - let probe_paths: Vec<(PathBuf, SloppyImportsResolutionReason)> = - match self.fs.stat_sync(&path) { - Some(SloppyImportsFsEntry::File) => { - if resolution_kind.is_types() { - let media_type = MediaType::from_specifier(specifier); - // attempt to resolve the .d.ts file before the .js file - let probe_media_type_types = match media_type { - MediaType::JavaScript => { - vec![(MediaType::Dts), MediaType::JavaScript] - } - MediaType::Mjs => { - vec![MediaType::Dmts, MediaType::Dts, MediaType::Mjs] - } - MediaType::Cjs => { - vec![MediaType::Dcts, MediaType::Dts, MediaType::Cjs] - } - _ => return None, - }; - let path_no_ext = path_without_ext(&path, media_type)?; - media_types_to_paths( - &path_no_ext, - media_type, - probe_media_type_types, - SloppyImportsResolutionReason::JsToTs, - ) - } else { - return None; - } - } - entry @ None | entry @ Some(SloppyImportsFsEntry::Dir) => { - let media_type = MediaType::from_specifier(specifier); - let probe_media_type_types = match media_type { - MediaType::JavaScript => ( - if resolution_kind.is_types() { - vec![MediaType::TypeScript, MediaType::Tsx, MediaType::Dts] - } else { - vec![MediaType::TypeScript, MediaType::Tsx] - }, - SloppyImportsResolutionReason::JsToTs, - ), - MediaType::Jsx => { - (vec![MediaType::Tsx], SloppyImportsResolutionReason::JsToTs) - } - MediaType::Mjs => ( - if resolution_kind.is_types() { - vec![MediaType::Mts, MediaType::Dmts, MediaType::Dts] - } else { - vec![MediaType::Mts] - }, - SloppyImportsResolutionReason::JsToTs, - ), - MediaType::Cjs => ( - if resolution_kind.is_types() { - vec![MediaType::Cts, MediaType::Dcts, MediaType::Dts] - } else { - vec![MediaType::Cts] - }, - SloppyImportsResolutionReason::JsToTs, - ), - MediaType::TypeScript - | MediaType::Mts - | MediaType::Cts - | MediaType::Dts - | MediaType::Dmts - | MediaType::Dcts - | MediaType::Tsx - | MediaType::Json - | MediaType::Wasm - | MediaType::Css - | MediaType::SourceMap => { - return None; - } - MediaType::Unknown => ( - if resolution_kind.is_types() { - vec![ - MediaType::TypeScript, - MediaType::Tsx, - MediaType::Mts, - MediaType::Dts, - MediaType::Dmts, - MediaType::Dcts, - MediaType::JavaScript, - MediaType::Jsx, - MediaType::Mjs, - ] - } else { - vec![ - MediaType::TypeScript, - MediaType::JavaScript, - MediaType::Tsx, - MediaType::Jsx, - MediaType::Mts, - MediaType::Mjs, - ] - }, - SloppyImportsResolutionReason::NoExtension, - ), - }; - let mut probe_paths = match path_without_ext(&path, media_type) { - Some(path_no_ext) => media_types_to_paths( - &path_no_ext, - media_type, - probe_media_type_types.0, - probe_media_type_types.1, - ), - None => vec![], - }; - - if matches!(entry, Some(SloppyImportsFsEntry::Dir)) { - // try to resolve at the index file - if resolution_kind.is_types() { - probe_paths.push(( - path.join("index.ts"), - SloppyImportsResolutionReason::Directory, - )); - - probe_paths.push(( - path.join("index.mts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.d.ts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.d.mts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.js"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.mjs"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.tsx"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.jsx"), - SloppyImportsResolutionReason::Directory, - )); - } else { - probe_paths.push(( - path.join("index.ts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.mts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.tsx"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.js"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.mjs"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.jsx"), - SloppyImportsResolutionReason::Directory, - )); - } - } - if probe_paths.is_empty() { - return None; - } - probe_paths - } - }; - - for (probe_path, reason) in probe_paths { - if self.fs.is_file(&probe_path) { - if let Ok(specifier) = url_from_file_path(&probe_path) { - match reason { - SloppyImportsResolutionReason::JsToTs => { - return Some(SloppyImportsResolution::JsToTs(specifier)); - } - SloppyImportsResolutionReason::NoExtension => { - return Some(SloppyImportsResolution::NoExtension(specifier)); - } - SloppyImportsResolutionReason::Directory => { - return Some(SloppyImportsResolution::Directory(specifier)); - } - } - } - } - } - - None - } -} - -#[derive(Debug)] -pub struct SloppyImportsCachedFs { - sys: TSys, - cache: Option>>, -} - -impl SloppyImportsCachedFs { - pub fn new(sys: TSys) -> Self { - Self { - sys, - cache: Some(Default::default()), - } - } - - pub fn new_without_stat_cache(sys: TSys) -> Self { - Self { sys, cache: None } - } -} - -impl SloppyImportResolverFs for SloppyImportsCachedFs { - fn stat_sync(&self, path: &Path) -> Option { - if let Some(cache) = &self.cache { - if let Some(entry) = cache.get(path) { - return *entry; - } - } - - let entry = self.sys.fs_metadata(path).ok().and_then(|stat| { - if stat.file_type().is_file() { - Some(SloppyImportsFsEntry::File) - } else if stat.file_type().is_dir() { - Some(SloppyImportsFsEntry::Dir) - } else { - None - } - }); - - if let Some(cache) = &self.cache { - cache.insert(path.to_owned(), entry); - } - entry - } -} - -#[cfg(test)] -mod test { - use test_util::TestContext; - - use super::*; - - #[test] - fn test_unstable_sloppy_imports() { - fn resolve(specifier: &Url) -> Option { - resolve_with_resolution_kind( - specifier, - SloppyImportsResolutionKind::Execution, - ) - } - - fn resolve_types(specifier: &Url) -> Option { - resolve_with_resolution_kind( - specifier, - SloppyImportsResolutionKind::Types, - ) - } - - fn resolve_with_resolution_kind( - specifier: &Url, - resolution_kind: SloppyImportsResolutionKind, - ) -> Option { - struct RealSloppyImportsResolverFs; - impl SloppyImportResolverFs for RealSloppyImportsResolverFs { - fn stat_sync(&self, path: &Path) -> Option { - #[allow(clippy::disallowed_methods)] - let stat = std::fs::metadata(path).ok()?; - if stat.is_dir() { - Some(SloppyImportsFsEntry::Dir) - } else if stat.is_file() { - Some(SloppyImportsFsEntry::File) - } else { - None - } - } - } - - SloppyImportsResolver::new(RealSloppyImportsResolverFs) - .resolve(specifier, resolution_kind) - } - - let context = TestContext::default(); - let temp_dir = context.temp_dir().path(); - - // scenarios like resolving ./example.js to ./example.ts - for (ext_from, ext_to) in [("js", "ts"), ("js", "tsx"), ("mjs", "mts")] { - let ts_file = temp_dir.join(format!("file.{}", ext_to)); - ts_file.write(""); - assert_eq!(resolve(&ts_file.url_file()), None); - assert_eq!( - resolve( - &temp_dir - .url_dir() - .join(&format!("file.{}", ext_from)) - .unwrap() - ), - Some(SloppyImportsResolution::JsToTs(ts_file.url_file())), - ); - ts_file.remove_file(); - } - - // no extension scenarios - for ext in ["js", "ts", "js", "tsx", "jsx", "mjs", "mts"] { - let file = temp_dir.join(format!("file.{}", ext)); - file.write(""); - assert_eq!( - resolve( - &temp_dir - .url_dir() - .join("file") // no ext - .unwrap() - ), - Some(SloppyImportsResolution::NoExtension(file.url_file())) - ); - file.remove_file(); - } - - // .ts and .js exists, .js specified (goes to specified) - { - let ts_file = temp_dir.join("file.ts"); - ts_file.write(""); - let js_file = temp_dir.join("file.js"); - js_file.write(""); - assert_eq!(resolve(&js_file.url_file()), None); - } - - // only js exists, .js specified - { - let js_only_file = temp_dir.join("js_only.js"); - js_only_file.write(""); - assert_eq!(resolve(&js_only_file.url_file()), None); - assert_eq!(resolve_types(&js_only_file.url_file()), None); - } - - // resolving a directory to an index file - { - let routes_dir = temp_dir.join("routes"); - routes_dir.create_dir_all(); - let index_file = routes_dir.join("index.ts"); - index_file.write(""); - assert_eq!( - resolve(&routes_dir.url_file()), - Some(SloppyImportsResolution::Directory(index_file.url_file())), - ); - } - - // both a directory and a file with specifier is present - { - let api_dir = temp_dir.join("api"); - api_dir.create_dir_all(); - let bar_file = api_dir.join("bar.ts"); - bar_file.write(""); - let api_file = temp_dir.join("api.ts"); - api_file.write(""); - assert_eq!( - resolve(&api_dir.url_file()), - Some(SloppyImportsResolution::NoExtension(api_file.url_file())), - ); - } - } - - #[test] - fn test_sloppy_import_resolution_suggestion_message() { - // directory - assert_eq!( - SloppyImportsResolution::Directory( - Url::parse("file:///dir/index.js").unwrap() - ) - .as_suggestion_message(), - "Maybe specify path to 'index.js' file in directory instead" - ); - // no ext - assert_eq!( - SloppyImportsResolution::NoExtension( - Url::parse("file:///dir/index.mjs").unwrap() - ) - .as_suggestion_message(), - "Maybe add a '.mjs' extension" - ); - // js to ts - assert_eq!( - SloppyImportsResolution::JsToTs( - Url::parse("file:///dir/index.mts").unwrap() - ) - .as_suggestion_message(), - "Maybe change the extension to '.mts'" - ); - } -} diff --git a/resolvers/deno/workspace.rs b/resolvers/deno/workspace.rs new file mode 100644 index 00000000000000..b7169497222435 --- /dev/null +++ b/resolvers/deno/workspace.rs @@ -0,0 +1,2812 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +// use super::UrlRc; + +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::fmt; +use std::path::Path; +use std::path::PathBuf; + +use deno_config::deno_json::ConfigFile; +use deno_config::deno_json::ConfigFileError; +use deno_config::workspace::ResolverWorkspaceJsrPackage; +use deno_config::workspace::Workspace; +use deno_error::JsError; +use deno_media_type::MediaType; +use deno_package_json::PackageJsonDepValue; +use deno_package_json::PackageJsonDepValueParseError; +use deno_package_json::PackageJsonDepWorkspaceReq; +use deno_package_json::PackageJsonDepsRc; +use deno_package_json::PackageJsonRc; +use deno_path_util::url_from_directory_path; +use deno_path_util::url_from_file_path; +use deno_path_util::url_to_file_path; +use deno_semver::jsr::JsrPackageReqReference; +use deno_semver::package::PackageReq; +use deno_semver::RangeSetOrTag; +use deno_semver::Version; +use deno_semver::VersionReq; +use import_map::specifier::SpecifierError; +use import_map::ImportMap; +use import_map::ImportMapDiagnostic; +use import_map::ImportMapError; +use import_map::ImportMapErrorKind; +use import_map::ImportMapWithDiagnostics; +use indexmap::IndexMap; +use node_resolver::NodeResolutionKind; +use serde::Deserialize; +use serde::Serialize; +use sys_traits::FsMetadata; +use sys_traits::FsMetadataValue; +use sys_traits::FsRead; +use thiserror::Error; +use url::Url; + +use crate::sync::new_rc; +use crate::sync::MaybeDashMap; + +#[allow(clippy::disallowed_types)] +type UrlRc = crate::sync::MaybeArc; + +#[derive(Debug)] +struct PkgJsonResolverFolderConfig { + deps: PackageJsonDepsRc, + pkg_json: PackageJsonRc, +} + +#[derive(Debug, Error, JsError)] +pub enum WorkspaceResolverCreateError { + #[class(inherit)] + #[error("Failed loading import map specified in '{referrer}'")] + ImportMapFetch { + referrer: Url, + #[source] + #[inherit] + source: Box, + }, + #[class(inherit)] + #[error(transparent)] + ImportMap( + #[from] + #[inherit] + ImportMapError, + ), +} + +/// Whether to resolve dependencies by reading the dependencies list +/// from a package.json +#[derive( + Debug, Default, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, +)] +pub enum PackageJsonDepResolution { + /// Resolves based on the dep entries in the package.json. + #[default] + Enabled, + /// Doesn't use the package.json to resolve dependencies. Let's the caller + /// resolve based on the file system. + Disabled, +} + +#[derive( + Debug, Default, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, +)] +pub enum SloppyImportsOptions { + Enabled, + #[default] + Disabled, +} + +/// Toggle FS metadata caching when probing files for sloppy imports and +/// `compilerOptions.rootDirs` resolution. +#[derive( + Debug, Default, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, +)] +pub enum FsCacheOptions { + #[default] + Enabled, + Disabled, +} + +#[derive(Debug, Default, Clone)] +pub struct CreateResolverOptions { + pub pkg_json_dep_resolution: PackageJsonDepResolution, + pub specified_import_map: Option, + pub sloppy_imports_options: SloppyImportsOptions, + pub fs_cache_options: FsCacheOptions, +} + +#[derive(Debug, Clone)] +pub struct SpecifiedImportMap { + pub base_url: Url, + pub value: serde_json::Value, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MappedResolutionDiagnostic { + ConstraintNotMatchedLocalVersion { + /// If it was for a patch (true) or workspace (false) member. + is_patch: bool, + reference: JsrPackageReqReference, + local_version: Version, + }, +} + +impl std::fmt::Display for MappedResolutionDiagnostic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ConstraintNotMatchedLocalVersion { + is_patch, + reference, + local_version, + } => { + write!( + f, + "{0} '{1}@{2}' was not used because it did not match '{1}@{3}'", + if *is_patch { + "Patch" + } else { + "Workspace member" + }, + reference.req().name, + local_version, + reference.req().version_req + ) + } + } + } +} + +#[derive(Debug, Clone)] +pub enum MappedResolution<'a> { + Normal { + specifier: Url, + used_import_map: bool, + sloppy_reason: Option, + used_compiler_options_root_dirs: bool, + maybe_diagnostic: Option>, + }, + WorkspaceJsrPackage { + specifier: Url, + pkg_req_ref: JsrPackageReqReference, + }, + /// Resolved a bare specifier to a package.json that was a workspace member. + WorkspaceNpmPackage { + target_pkg_json: &'a PackageJsonRc, + pkg_name: &'a str, + sub_path: Option, + }, + PackageJson { + pkg_json: &'a PackageJsonRc, + alias: &'a str, + sub_path: Option, + dep_result: &'a Result, + }, +} + +#[derive(Debug, Clone, Error, JsError)] +#[class(type)] +pub enum WorkspaceResolveError { + #[error("Failed joining '{}' to '{}'. {:#}", .sub_path, .base, .error)] + InvalidExportPath { + base: Url, + sub_path: String, + error: url::ParseError, + }, + #[error("Unknown export '{}' for '{}'.\n Package exports:\n{}", export_name, package_name, .exports.iter().map(|e| format!(" * {}", e)).collect::>().join("\n"))] + UnknownExport { + package_name: String, + export_name: String, + exports: Vec, + }, +} + +#[derive(Debug, Error, JsError)] +pub enum MappedResolutionError { + #[class(inherit)] + #[error(transparent)] + Specifier(#[from] SpecifierError), + #[class(inherit)] + #[error(transparent)] + ImportMap(#[from] ImportMapError), + #[class(inherit)] + #[error(transparent)] + Workspace(#[from] WorkspaceResolveError), +} + +impl MappedResolutionError { + pub fn is_unmapped_bare_specifier(&self) -> bool { + match self { + MappedResolutionError::Specifier(err) => match err { + SpecifierError::InvalidUrl(_) => false, + SpecifierError::ImportPrefixMissing { .. } => true, + }, + MappedResolutionError::ImportMap(err) => { + matches!(**err, ImportMapErrorKind::UnmappedBareSpecifier(_, _)) + } + MappedResolutionError::Workspace(_) => false, + } + } +} + +#[derive(Error, Debug, JsError)] +#[class(inherit)] +#[error(transparent)] +pub struct WorkspaceResolvePkgJsonFolderError( + Box, +); + +impl WorkspaceResolvePkgJsonFolderError { + pub fn as_kind(&self) -> &WorkspaceResolvePkgJsonFolderErrorKind { + &self.0 + } + + pub fn into_kind(self) -> WorkspaceResolvePkgJsonFolderErrorKind { + *self.0 + } +} + +impl From for WorkspaceResolvePkgJsonFolderError +where + WorkspaceResolvePkgJsonFolderErrorKind: From, +{ + fn from(err: E) -> Self { + WorkspaceResolvePkgJsonFolderError(Box::new( + WorkspaceResolvePkgJsonFolderErrorKind::from(err), + )) + } +} + +#[derive(Debug, Error, JsError, Clone, PartialEq, Eq)] +#[class(type)] +pub enum WorkspaceResolvePkgJsonFolderErrorKind { + #[error("Could not find package.json with name '{0}' in workspace.")] + NotFound(String), + #[error("Found package.json in workspace, but version '{1}' didn't satisy constraint '{0}'.")] + VersionNotSatisfied(VersionReq, Version), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CachedMetadataFsEntry { + File, + Dir, +} + +#[derive(Debug)] +struct CachedMetadataFs { + sys: TSys, + cache: Option>>, +} + +impl CachedMetadataFs { + fn new(sys: TSys, options: FsCacheOptions) -> Self { + Self { + sys, + cache: match options { + FsCacheOptions::Enabled => Some(Default::default()), + FsCacheOptions::Disabled => None, + }, + } + } + + fn stat_sync(&self, path: &Path) -> Option { + if let Some(cache) = &self.cache { + if let Some(entry) = cache.get(path) { + return *entry; + } + } + let entry = self.sys.fs_metadata(path).ok().and_then(|stat| { + if stat.file_type().is_file() { + Some(CachedMetadataFsEntry::File) + } else if stat.file_type().is_dir() { + Some(CachedMetadataFsEntry::Dir) + } else { + None + } + }); + if let Some(cache) = &self.cache { + cache.insert(path.to_owned(), entry); + } + entry + } + + fn is_file(&self, path: &Path) -> bool { + self.stat_sync(path) == Some(CachedMetadataFsEntry::File) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SloppyImportsResolutionReason { + /// Ex. `./file.js` to `./file.ts` + JsToTs, + /// Ex. `./file` to `./file.ts` + NoExtension, + /// Ex. `./dir` to `./dir/index.ts` + Directory, +} + +impl SloppyImportsResolutionReason { + pub fn suggestion_message_for_specifier(&self, specifier: &Url) -> String { + format!("Maybe {}", self.base_message_for_specifier(specifier)) + } + + pub fn quick_fix_message_for_specifier(&self, specifier: &Url) -> String { + let message = self.base_message_for_specifier(specifier); + let mut chars = message.chars(); + format!( + "{}{}.", + chars.next().unwrap().to_uppercase(), + chars.as_str() + ) + } + + fn base_message_for_specifier(&self, specifier: &Url) -> String { + match self { + Self::JsToTs => { + let media_type = MediaType::from_specifier(specifier); + format!("change the extension to '{}'", media_type.as_ts_extension()) + } + Self::NoExtension => { + let media_type = MediaType::from_specifier(specifier); + format!("add a '{}' extension", media_type.as_ts_extension()) + } + Self::Directory => { + let file_name = specifier + .path() + .rsplit_once('/') + .map(|(_, file_name)| file_name) + .unwrap_or(specifier.path()); + format!("specify path to '{}' file in directory instead", file_name) + } + } + } +} + +#[derive(Debug)] +struct SloppyImportsResolver { + fs: CachedMetadataFs, + enabled: bool, +} + +impl SloppyImportsResolver { + fn new(fs: CachedMetadataFs, options: SloppyImportsOptions) -> Self { + Self { + fs, + enabled: match options { + SloppyImportsOptions::Enabled => true, + SloppyImportsOptions::Disabled => false, + }, + } + } + + fn resolve( + &self, + specifier: &Url, + resolution_kind: ResolutionKind, + ) -> Option<(Url, SloppyImportsResolutionReason)> { + if !self.enabled { + return None; + } + + fn path_without_ext( + path: &Path, + media_type: MediaType, + ) -> Option> { + let old_path_str = path.to_string_lossy(); + match media_type { + MediaType::Unknown => Some(old_path_str), + _ => old_path_str + .strip_suffix(media_type.as_ts_extension()) + .map(|s| Cow::Owned(s.to_string())), + } + } + + fn media_types_to_paths( + path_no_ext: &str, + original_media_type: MediaType, + probe_media_type_types: Vec, + reason: SloppyImportsResolutionReason, + ) -> Vec<(PathBuf, SloppyImportsResolutionReason)> { + probe_media_type_types + .into_iter() + .filter(|media_type| *media_type != original_media_type) + .map(|media_type| { + ( + PathBuf::from(format!( + "{}{}", + path_no_ext, + media_type.as_ts_extension() + )), + reason, + ) + }) + .collect::>() + } + + if specifier.scheme() != "file" { + return None; + } + + let path = url_to_file_path(specifier).ok()?; + + let probe_paths: Vec<(PathBuf, SloppyImportsResolutionReason)> = + match self.fs.stat_sync(&path) { + Some(CachedMetadataFsEntry::File) => { + if resolution_kind.is_types() { + let media_type = MediaType::from_specifier(specifier); + // attempt to resolve the .d.ts file before the .js file + let probe_media_type_types = match media_type { + MediaType::JavaScript => { + vec![(MediaType::Dts), MediaType::JavaScript] + } + MediaType::Mjs => { + vec![MediaType::Dmts, MediaType::Dts, MediaType::Mjs] + } + MediaType::Cjs => { + vec![MediaType::Dcts, MediaType::Dts, MediaType::Cjs] + } + _ => return None, + }; + let path_no_ext = path_without_ext(&path, media_type)?; + media_types_to_paths( + &path_no_ext, + media_type, + probe_media_type_types, + SloppyImportsResolutionReason::JsToTs, + ) + } else { + return None; + } + } + entry @ None | entry @ Some(CachedMetadataFsEntry::Dir) => { + let media_type = MediaType::from_specifier(specifier); + let probe_media_type_types = match media_type { + MediaType::JavaScript => ( + if resolution_kind.is_types() { + vec![MediaType::TypeScript, MediaType::Tsx, MediaType::Dts] + } else { + vec![MediaType::TypeScript, MediaType::Tsx] + }, + SloppyImportsResolutionReason::JsToTs, + ), + MediaType::Jsx => { + (vec![MediaType::Tsx], SloppyImportsResolutionReason::JsToTs) + } + MediaType::Mjs => ( + if resolution_kind.is_types() { + vec![MediaType::Mts, MediaType::Dmts, MediaType::Dts] + } else { + vec![MediaType::Mts] + }, + SloppyImportsResolutionReason::JsToTs, + ), + MediaType::Cjs => ( + if resolution_kind.is_types() { + vec![MediaType::Cts, MediaType::Dcts, MediaType::Dts] + } else { + vec![MediaType::Cts] + }, + SloppyImportsResolutionReason::JsToTs, + ), + MediaType::TypeScript + | MediaType::Mts + | MediaType::Cts + | MediaType::Dts + | MediaType::Dmts + | MediaType::Dcts + | MediaType::Tsx + | MediaType::Json + | MediaType::Wasm + | MediaType::Css + | MediaType::SourceMap => { + return None; + } + MediaType::Unknown => ( + if resolution_kind.is_types() { + vec![ + MediaType::TypeScript, + MediaType::Tsx, + MediaType::Mts, + MediaType::Dts, + MediaType::Dmts, + MediaType::Dcts, + MediaType::JavaScript, + MediaType::Jsx, + MediaType::Mjs, + ] + } else { + vec![ + MediaType::TypeScript, + MediaType::JavaScript, + MediaType::Tsx, + MediaType::Jsx, + MediaType::Mts, + MediaType::Mjs, + ] + }, + SloppyImportsResolutionReason::NoExtension, + ), + }; + let mut probe_paths = match path_without_ext(&path, media_type) { + Some(path_no_ext) => media_types_to_paths( + &path_no_ext, + media_type, + probe_media_type_types.0, + probe_media_type_types.1, + ), + None => vec![], + }; + + if matches!(entry, Some(CachedMetadataFsEntry::Dir)) { + // try to resolve at the index file + if resolution_kind.is_types() { + probe_paths.push(( + path.join("index.ts"), + SloppyImportsResolutionReason::Directory, + )); + + probe_paths.push(( + path.join("index.mts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.d.ts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.d.mts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.js"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.mjs"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.tsx"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.jsx"), + SloppyImportsResolutionReason::Directory, + )); + } else { + probe_paths.push(( + path.join("index.ts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.mts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.tsx"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.js"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.mjs"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.jsx"), + SloppyImportsResolutionReason::Directory, + )); + } + } + if probe_paths.is_empty() { + return None; + } + probe_paths + } + }; + + for (probe_path, reason) in probe_paths { + if self.fs.is_file(&probe_path) { + if let Ok(specifier) = url_from_file_path(&probe_path) { + return Some((specifier, reason)); + } + } + } + + None + } +} + +pub fn sloppy_imports_resolve( + specifier: &Url, + resolution_kind: ResolutionKind, + sys: TSys, +) -> Option<(Url, SloppyImportsResolutionReason)> { + SloppyImportsResolver::new( + CachedMetadataFs::new(sys, FsCacheOptions::Enabled), + SloppyImportsOptions::Enabled, + ) + .resolve(specifier, resolution_kind) +} + +#[allow(clippy::disallowed_types)] +type SloppyImportsResolverRc = + crate::sync::MaybeArc>; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CompilerOptionsRootDirsDiagnostic { + InvalidType(Url), + InvalidEntryType(Url, usize), + UnexpectedError(Url, String), + UnexpectedEntryError(Url, usize, String), +} + +impl fmt::Display for CompilerOptionsRootDirsDiagnostic { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::InvalidType(s) => write!(f, "Invalid value for \"compilerOptions.rootDirs\" (\"{s}\"). Expected a string."), + Self::InvalidEntryType(s, i) => write!(f, "Invalid value for \"compilerOptions.rootDirs[{i}]\" (\"{s}\"). Expected a string."), + Self::UnexpectedError(s, message) => write!(f, "Unexpected error while parsing \"compilerOptions.rootDirs\" (\"{s}\"): {message}"), + Self::UnexpectedEntryError(s, i, message) => write!(f, "Unexpected error while parsing \"compilerOptions.rootDirs[{i}]\" (\"{s}\"): {message}"), + } + } +} + +#[derive(Debug)] +struct CompilerOptionsRootDirsResolver { + root_dirs_from_root: Vec, + root_dirs_by_member: BTreeMap>>, + diagnostics: Vec, + sloppy_imports_resolver: SloppyImportsResolverRc, +} + +impl CompilerOptionsRootDirsResolver { + fn from_workspace( + workspace: &Workspace, + sloppy_imports_resolver: SloppyImportsResolverRc, + ) -> Self { + let mut diagnostics: Vec = Vec::new(); + fn get_root_dirs( + config_file: &ConfigFile, + dir_url: &Url, + diagnostics: &mut Vec, + ) -> Option> { + let dir_path = url_to_file_path(dir_url) + .inspect_err(|err| { + diagnostics.push(CompilerOptionsRootDirsDiagnostic::UnexpectedError( + config_file.specifier.clone(), + err.to_string(), + )); + }) + .ok()?; + let root_dirs = config_file + .json + .compiler_options + .as_ref()? + .as_object()? + .get("rootDirs")? + .as_array(); + if root_dirs.is_none() { + diagnostics.push(CompilerOptionsRootDirsDiagnostic::InvalidType( + config_file.specifier.clone(), + )); + } + let root_dirs = root_dirs? + .iter() + .enumerate() + .filter_map(|(i, s)| { + let s = s.as_str(); + if s.is_none() { + diagnostics.push( + CompilerOptionsRootDirsDiagnostic::InvalidEntryType( + config_file.specifier.clone(), + i, + ), + ); + } + url_from_directory_path(&dir_path.join(s?)) + .inspect_err(|err| { + diagnostics.push( + CompilerOptionsRootDirsDiagnostic::UnexpectedEntryError( + config_file.specifier.clone(), + i, + err.to_string(), + ), + ); + }) + .ok() + }) + .collect(); + Some(root_dirs) + } + let root_deno_json = workspace.root_deno_json(); + let root_dirs_from_root = root_deno_json + .and_then(|c| { + let root_dir_url = c + .specifier + .join(".") + .inspect_err(|err| { + diagnostics.push( + CompilerOptionsRootDirsDiagnostic::UnexpectedError( + c.specifier.clone(), + err.to_string(), + ), + ); + }) + .ok()?; + get_root_dirs(c, &root_dir_url, &mut diagnostics) + }) + .unwrap_or_default(); + let root_dirs_by_member = workspace + .resolver_deno_jsons() + .filter_map(|c| { + if let Some(root_deno_json) = root_deno_json { + if c.specifier == root_deno_json.specifier { + return None; + } + } + let dir_url = c + .specifier + .join(".") + .inspect_err(|err| { + diagnostics.push( + CompilerOptionsRootDirsDiagnostic::UnexpectedError( + c.specifier.clone(), + err.to_string(), + ), + ); + }) + .ok()?; + let root_dirs = get_root_dirs(c, &dir_url, &mut diagnostics); + Some((dir_url, root_dirs)) + }) + .collect(); + Self { + root_dirs_from_root, + root_dirs_by_member, + diagnostics, + sloppy_imports_resolver, + } + } + + fn new_raw( + root_dirs_from_root: Vec, + root_dirs_by_member: BTreeMap>>, + sloppy_imports_resolver: SloppyImportsResolverRc, + ) -> Self { + Self { + root_dirs_from_root, + root_dirs_by_member, + diagnostics: Default::default(), + sloppy_imports_resolver, + } + } + + fn resolve_types( + &self, + specifier: &Url, + referrer: &Url, + ) -> Option<(Url, Option)> { + if specifier.scheme() != "file" || referrer.scheme() != "file" { + return None; + } + let root_dirs = self + .root_dirs_by_member + .iter() + .rfind(|(s, _)| referrer.as_str().starts_with(s.as_str())) + .and_then(|(_, r)| r.as_ref()) + .unwrap_or(&self.root_dirs_from_root); + let (matched_root_dir, suffix) = root_dirs + .iter() + .filter_map(|r| { + let suffix = specifier.as_str().strip_prefix(r.as_str())?; + Some((r, suffix)) + }) + .max_by_key(|(r, _)| r.as_str().len())?; + for root_dir in root_dirs { + if root_dir == matched_root_dir { + continue; + } + let Ok(candidate_specifier) = root_dir.join(suffix) else { + continue; + }; + let Ok(candidate_path) = url_to_file_path(&candidate_specifier) else { + continue; + }; + if self.sloppy_imports_resolver.fs.is_file(&candidate_path) { + return Some((candidate_specifier, None)); + } else if let Some((candidate_specifier, sloppy_reason)) = self + .sloppy_imports_resolver + .resolve(&candidate_specifier, ResolutionKind::Types) + { + return Some((candidate_specifier, Some(sloppy_reason))); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ResolutionKind { + /// Resolving for code that will be executed. + Execution, + /// Resolving for code that will be used for type information. + Types, +} + +impl ResolutionKind { + pub fn is_types(&self) -> bool { + *self == ResolutionKind::Types + } +} + +impl From for ResolutionKind { + fn from(value: NodeResolutionKind) -> Self { + match value { + NodeResolutionKind::Execution => Self::Execution, + NodeResolutionKind::Types => Self::Types, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WorkspaceResolverDiagnostic<'a> { + ImportMap(&'a ImportMapDiagnostic), + CompilerOptionsRootDirs(&'a CompilerOptionsRootDirsDiagnostic), +} + +impl fmt::Display for WorkspaceResolverDiagnostic<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ImportMap(d) => write!(f, "Import map: {d}"), + Self::CompilerOptionsRootDirs(d) => d.fmt(f), + } + } +} + +#[derive(Debug)] +pub struct WorkspaceResolver { + workspace_root: UrlRc, + jsr_pkgs: Vec, + maybe_import_map: Option, + pkg_jsons: BTreeMap, + pkg_json_dep_resolution: PackageJsonDepResolution, + sloppy_imports_options: SloppyImportsOptions, + fs_cache_options: FsCacheOptions, + sloppy_imports_resolver: SloppyImportsResolverRc, + compiler_options_root_dirs_resolver: CompilerOptionsRootDirsResolver, +} + +impl WorkspaceResolver { + pub fn from_workspace( + workspace: &Workspace, + sys: TSys, + options: CreateResolverOptions, + ) -> Result { + fn resolve_import_map( + sys: &impl FsRead, + workspace: &Workspace, + specified_import_map: Option, + ) -> Result, WorkspaceResolverCreateError> + { + let root_deno_json = workspace.root_deno_json(); + let deno_jsons = workspace.resolver_deno_jsons().collect::>(); + + let (import_map_url, import_map) = match specified_import_map { + Some(SpecifiedImportMap { + base_url, + value: import_map, + }) => (base_url, import_map), + None => { + if !deno_jsons.iter().any(|p| p.is_package()) + && !deno_jsons.iter().any(|c| { + c.json.import_map.is_some() + || c.json.scopes.is_some() + || c.json.imports.is_some() + || c + .json + .compiler_options + .as_ref() + .and_then(|v| v.as_object()?.get("rootDirs")?.as_array()) + .is_some_and(|a| a.len() > 1) + }) + { + // no configs have an import map and none are a package, so exit + return Ok(None); + } + + let config_specified_import_map = match root_deno_json.as_ref() { + Some(deno_json) => deno_json + .to_import_map_value(sys) + .map_err(|source| WorkspaceResolverCreateError::ImportMapFetch { + referrer: deno_json.specifier.clone(), + source: Box::new(source), + })? + .unwrap_or_else(|| { + ( + Cow::Borrowed(&deno_json.specifier), + serde_json::Value::Object(Default::default()), + ) + }), + None => ( + Cow::Owned(workspace.root_dir().join("deno.json").unwrap()), + serde_json::Value::Object(Default::default()), + ), + }; + let base_import_map_config = import_map::ext::ImportMapConfig { + base_url: config_specified_import_map.0.into_owned(), + import_map_value: config_specified_import_map.1, + }; + let child_import_map_configs = deno_jsons + .iter() + .filter(|f| { + Some(&f.specifier) + != root_deno_json.as_ref().map(|c| &c.specifier) + }) + .map(|config| import_map::ext::ImportMapConfig { + base_url: config.specifier.clone(), + import_map_value: { + // don't include scopes here + let mut value = serde_json::Map::with_capacity(1); + if let Some(imports) = &config.json.imports { + value.insert("imports".to_string(), imports.clone()); + } + value.into() + }, + }) + .collect::>(); + let (import_map_url, import_map) = + ::import_map::ext::create_synthetic_import_map( + base_import_map_config, + child_import_map_configs, + ); + let import_map = import_map::ext::expand_import_map_value(import_map); + log::debug!( + "Workspace config generated this import map {}", + serde_json::to_string_pretty(&import_map).unwrap() + ); + (import_map_url, import_map) + } + }; + Ok(Some(import_map::parse_from_value( + import_map_url, + import_map, + )?)) + } + + let maybe_import_map = + resolve_import_map(&sys, workspace, options.specified_import_map)?; + let jsr_pkgs = workspace.resolver_jsr_pkgs().collect::>(); + let pkg_jsons = workspace + .resolver_pkg_jsons() + .map(|(dir_url, pkg_json)| { + let deps = pkg_json.resolve_local_package_json_deps(); + ( + dir_url.clone(), + PkgJsonResolverFolderConfig { + deps: deps.clone(), + pkg_json: pkg_json.clone(), + }, + ) + }) + .collect::>(); + + let fs = CachedMetadataFs::new(sys, options.fs_cache_options); + let sloppy_imports_resolver = new_rc(SloppyImportsResolver::new( + fs, + options.sloppy_imports_options, + )); + let compiler_options_root_dirs_resolver = + CompilerOptionsRootDirsResolver::from_workspace( + workspace, + sloppy_imports_resolver.clone(), + ); + + Ok(Self { + workspace_root: workspace.root_dir().clone(), + pkg_json_dep_resolution: options.pkg_json_dep_resolution, + jsr_pkgs, + maybe_import_map, + pkg_jsons, + sloppy_imports_options: options.sloppy_imports_options, + fs_cache_options: options.fs_cache_options, + sloppy_imports_resolver, + compiler_options_root_dirs_resolver, + }) + } + + /// Creates a new WorkspaceResolver from the specified import map and package.jsons. + /// + /// Generally, create this from a Workspace instead. + #[allow(clippy::too_many_arguments)] + pub fn new_raw( + workspace_root: UrlRc, + maybe_import_map: Option, + jsr_pkgs: Vec, + pkg_jsons: Vec, + pkg_json_dep_resolution: PackageJsonDepResolution, + sloppy_imports_options: SloppyImportsOptions, + fs_cache_options: FsCacheOptions, + root_dirs_from_root: Vec, + root_dirs_by_member: BTreeMap>>, + sys: TSys, + ) -> Self { + let maybe_import_map = + maybe_import_map.map(|import_map| ImportMapWithDiagnostics { + import_map, + diagnostics: Default::default(), + }); + let pkg_jsons = pkg_jsons + .into_iter() + .map(|pkg_json| { + let deps = pkg_json.resolve_local_package_json_deps(); + ( + new_rc( + url_from_directory_path(pkg_json.path.parent().unwrap()).unwrap(), + ), + PkgJsonResolverFolderConfig { + deps: deps.clone(), + pkg_json, + }, + ) + }) + .collect::>(); + let fs = CachedMetadataFs::new(sys, fs_cache_options); + let sloppy_imports_resolver = + new_rc(SloppyImportsResolver::new(fs, sloppy_imports_options)); + let compiler_options_root_dirs_resolver = + CompilerOptionsRootDirsResolver::new_raw( + root_dirs_from_root, + root_dirs_by_member, + sloppy_imports_resolver.clone(), + ); + Self { + workspace_root, + jsr_pkgs, + maybe_import_map, + pkg_jsons, + pkg_json_dep_resolution, + sloppy_imports_options, + fs_cache_options, + sloppy_imports_resolver, + compiler_options_root_dirs_resolver, + } + } + + /// Prepare the workspace resolver for serialization + /// + /// The most significant preparation involves converting + /// absolute paths into relative (based on `root_dir_url`). + /// It also takes care of pre-serializing non-serde internal data. + pub fn to_serializable( + &self, + root_dir_url: &Url, + ) -> SerializableWorkspaceResolver { + let root_dir_url = BaseUrl(root_dir_url); + SerializableWorkspaceResolver { + import_map: self.maybe_import_map().map(|i| { + SerializedWorkspaceResolverImportMap { + specifier: root_dir_url.make_relative_if_descendant(i.base_url()), + json: Cow::Owned(i.to_json()), + } + }), + jsr_pkgs: self + .jsr_packages() + .map(|pkg| SerializedResolverWorkspaceJsrPackage { + relative_base: root_dir_url.make_relative_if_descendant(&pkg.base), + name: Cow::Borrowed(&pkg.name), + version: Cow::Borrowed(&pkg.version), + exports: Cow::Borrowed(&pkg.exports), + }) + .collect(), + package_jsons: self + .package_jsons() + .map(|pkg_json| { + ( + root_dir_url + .make_relative_if_descendant(&pkg_json.specifier()) + .into_owned(), + serde_json::to_value(pkg_json).unwrap(), + ) + }) + .collect(), + pkg_json_resolution: self.pkg_json_dep_resolution(), + sloppy_imports_options: self.sloppy_imports_options, + fs_cache_options: self.fs_cache_options, + root_dirs_from_root: self + .compiler_options_root_dirs_resolver + .root_dirs_from_root + .iter() + .map(|s| root_dir_url.make_relative_if_descendant(s)) + .collect(), + root_dirs_by_member: self + .compiler_options_root_dirs_resolver + .root_dirs_by_member + .iter() + .map(|(s, r)| { + ( + root_dir_url.make_relative_if_descendant(s), + r.as_ref().map(|r| { + r.iter() + .map(|s| root_dir_url.make_relative_if_descendant(s)) + .collect() + }), + ) + }) + .collect(), + } + } + + /// Deserialize a `WorkspaceResolver` + /// + /// Deserialization of `WorkspaceResolver`s is made in two steps. First + /// the serialized data must be deserialized in to `SerializableWorkspaceResolver` + /// (usually with serde), and then this method converts it into a `WorkspaceResolver`. + /// + /// This second step involves mainly converting the relative paths within + /// `SerializableWorkspaceResolver` into absolute paths using `root_dir_url`. + pub fn try_from_serializable( + root_dir_url: Url, + serializable_workspace_resolver: SerializableWorkspaceResolver, + sys: TSys, + ) -> Result { + let import_map = match serializable_workspace_resolver.import_map { + Some(import_map) => Some( + import_map::parse_from_json_with_options( + root_dir_url.join(&import_map.specifier).unwrap(), + &import_map.json, + import_map::ImportMapOptions { + address_hook: None, + expand_imports: true, + }, + )? + .import_map, + ), + None => None, + }; + let pkg_jsons = serializable_workspace_resolver + .package_jsons + .into_iter() + .map(|(relative_path, json)| { + let path = + url_to_file_path(&root_dir_url.join(&relative_path).unwrap()) + .unwrap(); + let pkg_json = + deno_package_json::PackageJson::load_from_value(path, json); + PackageJsonRc::new(pkg_json) + }) + .collect(); + let jsr_packages = serializable_workspace_resolver + .jsr_pkgs + .into_iter() + .map(|pkg| ResolverWorkspaceJsrPackage { + is_patch: false, // only used for enhancing the diagnostics, which are discarded when serializing + base: root_dir_url.join(&pkg.relative_base).unwrap(), + name: pkg.name.into_owned(), + version: pkg.version.into_owned(), + exports: pkg.exports.into_owned(), + }) + .collect(); + let root_dirs_from_root = serializable_workspace_resolver + .root_dirs_from_root + .iter() + .map(|s| root_dir_url.join(s).unwrap()) + .collect(); + let root_dirs_by_member = serializable_workspace_resolver + .root_dirs_by_member + .iter() + .map(|(s, r)| { + ( + root_dir_url.join(s).unwrap(), + r.as_ref() + .map(|r| r.iter().map(|s| root_dir_url.join(s).unwrap()).collect()), + ) + }) + .collect(); + Ok(Self::new_raw( + UrlRc::new(root_dir_url), + import_map, + jsr_packages, + pkg_jsons, + serializable_workspace_resolver.pkg_json_resolution, + serializable_workspace_resolver.sloppy_imports_options, + serializable_workspace_resolver.fs_cache_options, + root_dirs_from_root, + root_dirs_by_member, + sys, + )) + } + + pub fn maybe_import_map(&self) -> Option<&ImportMap> { + self.maybe_import_map.as_ref().map(|c| &c.import_map) + } + + pub fn package_jsons(&self) -> impl Iterator { + self.pkg_jsons.values().map(|c| &c.pkg_json) + } + + pub fn jsr_packages( + &self, + ) -> impl Iterator { + self.jsr_pkgs.iter() + } + + pub fn diagnostics(&self) -> Vec> { + self + .compiler_options_root_dirs_resolver + .diagnostics + .iter() + .map(WorkspaceResolverDiagnostic::CompilerOptionsRootDirs) + .chain( + self + .maybe_import_map + .as_ref() + .iter() + .flat_map(|c| &c.diagnostics) + .map(WorkspaceResolverDiagnostic::ImportMap), + ) + .collect() + } + + pub fn resolve<'a>( + &'a self, + specifier: &str, + referrer: &Url, + resolution_kind: ResolutionKind, + ) -> Result, MappedResolutionError> { + // 1. Attempt to resolve with the import map and normally first + let mut used_import_map = false; + let resolve_result = if let Some(import_map) = &self.maybe_import_map { + used_import_map = true; + import_map + .import_map + .resolve(specifier, referrer) + .map_err(MappedResolutionError::ImportMap) + } else { + import_map::specifier::resolve_import(specifier, referrer) + .map_err(MappedResolutionError::Specifier) + }; + let resolve_error = match resolve_result { + Ok(mut specifier) => { + let mut used_compiler_options_root_dirs = false; + let mut sloppy_reason = None; + if let Some((probed_specifier, probed_sloppy_reason)) = self + .sloppy_imports_resolver + .resolve(&specifier, resolution_kind) + { + specifier = probed_specifier; + sloppy_reason = Some(probed_sloppy_reason); + } else if resolution_kind.is_types() { + if let Some((probed_specifier, probed_sloppy_reason)) = self + .compiler_options_root_dirs_resolver + .resolve_types(&specifier, referrer) + { + used_compiler_options_root_dirs = true; + specifier = probed_specifier; + sloppy_reason = probed_sloppy_reason; + } + } + return self.maybe_resolve_specifier_to_workspace_jsr_pkg( + MappedResolution::Normal { + specifier, + used_import_map, + used_compiler_options_root_dirs, + sloppy_reason, + maybe_diagnostic: None, + }, + ); + } + Err(err) => err, + }; + + // 2. Try to resolve the bare specifier to a workspace member + if resolve_error.is_unmapped_bare_specifier() { + for member in &self.jsr_pkgs { + if let Some(path) = specifier.strip_prefix(&member.name) { + if path.is_empty() || path.starts_with('/') { + let path = path.strip_prefix('/').unwrap_or(path); + let pkg_req_ref = match JsrPackageReqReference::from_str(&format!( + "jsr:{}{}/{}", + member.name, + member + .version + .as_ref() + .map(|v| format!("@^{}", v)) + .unwrap_or_else(String::new), + path + )) { + Ok(pkg_req_ref) => pkg_req_ref, + Err(_) => { + // Ignore the error as it will be surfaced as a diagnostic + // in workspace.diagnostics() routine. + continue; + } + }; + return self.resolve_workspace_jsr_pkg(member, pkg_req_ref); + } + } + } + } + + if self.pkg_json_dep_resolution == PackageJsonDepResolution::Enabled { + // 2. Attempt to resolve from the package.json dependencies. + let mut previously_found_dir = false; + for (dir_url, pkg_json_folder) in self.pkg_jsons.iter().rev() { + if !referrer.as_str().starts_with(dir_url.as_str()) { + if previously_found_dir { + break; + } else { + continue; + } + } + previously_found_dir = true; + + for (bare_specifier, dep_result) in pkg_json_folder + .deps + .dependencies + .iter() + .chain(pkg_json_folder.deps.dev_dependencies.iter()) + { + if let Some(path) = specifier.strip_prefix(bare_specifier.as_str()) { + if path.is_empty() || path.starts_with('/') { + let sub_path = path.strip_prefix('/').unwrap_or(path); + return Ok(MappedResolution::PackageJson { + pkg_json: &pkg_json_folder.pkg_json, + alias: bare_specifier, + sub_path: if sub_path.is_empty() { + None + } else { + Some(sub_path.to_string()) + }, + dep_result, + }); + } + } + } + } + + // 3. Finally try to resolve to a workspace npm package if inside the workspace. + if referrer.as_str().starts_with(self.workspace_root.as_str()) { + for pkg_json_folder in self.pkg_jsons.values() { + let Some(name) = &pkg_json_folder.pkg_json.name else { + continue; + }; + let Some(path) = specifier.strip_prefix(name) else { + continue; + }; + if path.is_empty() || path.starts_with('/') { + let sub_path = path.strip_prefix('/').unwrap_or(path); + return Ok(MappedResolution::WorkspaceNpmPackage { + target_pkg_json: &pkg_json_folder.pkg_json, + pkg_name: name, + sub_path: if sub_path.is_empty() { + None + } else { + Some(sub_path.to_string()) + }, + }); + } + } + } + } + + // wasn't found, so surface the initial resolve error + Err(resolve_error) + } + + fn maybe_resolve_specifier_to_workspace_jsr_pkg<'a>( + &'a self, + resolution: MappedResolution<'a>, + ) -> Result, MappedResolutionError> { + let specifier = match resolution { + MappedResolution::Normal { ref specifier, .. } => specifier, + _ => return Ok(resolution), + }; + if specifier.scheme() != "jsr" { + return Ok(resolution); + } + let mut maybe_diagnostic = None; + if let Ok(package_req_ref) = + JsrPackageReqReference::from_specifier(specifier) + { + for pkg in &self.jsr_pkgs { + if pkg.name == package_req_ref.req().name { + if let Some(version) = &pkg.version { + if package_req_ref.req().version_req.matches(version) { + return self.resolve_workspace_jsr_pkg(pkg, package_req_ref); + } else { + maybe_diagnostic = Some(Box::new( + MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { + is_patch: pkg.is_patch, + reference: package_req_ref.clone(), + local_version: version.clone(), + }, + )); + } + } else { + // always resolve to workspace packages with no version + return self.resolve_workspace_jsr_pkg(pkg, package_req_ref); + } + } + } + } + Ok(match resolution { + MappedResolution::Normal { + specifier, + used_import_map, + used_compiler_options_root_dirs, + sloppy_reason, + .. + } => MappedResolution::Normal { + specifier, + used_import_map, + used_compiler_options_root_dirs, + sloppy_reason, + maybe_diagnostic, + }, + _ => return Ok(resolution), + }) + } + + fn resolve_workspace_jsr_pkg<'a>( + &'a self, + pkg: &'a ResolverWorkspaceJsrPackage, + pkg_req_ref: JsrPackageReqReference, + ) -> Result, MappedResolutionError> { + let export_name = pkg_req_ref.export_name(); + match pkg.exports.get(export_name.as_ref()) { + Some(sub_path) => match pkg.base.join(sub_path) { + Ok(specifier) => Ok(MappedResolution::WorkspaceJsrPackage { + specifier, + pkg_req_ref, + }), + Err(err) => Err( + WorkspaceResolveError::InvalidExportPath { + base: pkg.base.clone(), + sub_path: sub_path.to_string(), + error: err, + } + .into(), + ), + }, + None => Err( + WorkspaceResolveError::UnknownExport { + package_name: pkg.name.clone(), + export_name: export_name.to_string(), + exports: pkg.exports.keys().cloned().collect(), + } + .into(), + ), + } + } + + pub fn resolve_workspace_pkg_json_folder_for_npm_specifier( + &self, + pkg_req: &PackageReq, + ) -> Option<&Path> { + if pkg_req.version_req.tag().is_some() { + return None; + } + + self + .resolve_workspace_pkg_json_folder_for_pkg_json_dep( + &pkg_req.name, + &PackageJsonDepWorkspaceReq::VersionReq(pkg_req.version_req.clone()), + ) + .ok() + } + + pub fn resolve_workspace_pkg_json_folder_for_pkg_json_dep( + &self, + name: &str, + workspace_version_req: &PackageJsonDepWorkspaceReq, + ) -> Result<&Path, WorkspaceResolvePkgJsonFolderError> { + // this is not conditional on pkg_json_dep_resolution because we want + // to be able to do this resolution to figure out mapping an npm specifier + // to a workspace folder when using BYONM + let pkg_json = self + .package_jsons() + .find(|p| p.name.as_deref() == Some(name)); + let Some(pkg_json) = pkg_json else { + return Err( + WorkspaceResolvePkgJsonFolderErrorKind::NotFound(name.to_string()) + .into(), + ); + }; + match workspace_version_req { + PackageJsonDepWorkspaceReq::VersionReq(version_req) => { + match version_req.inner() { + RangeSetOrTag::RangeSet(set) => { + if let Some(version) = pkg_json + .version + .as_ref() + .and_then(|v| Version::parse_from_npm(v).ok()) + { + if set.satisfies(&version) { + Ok(pkg_json.dir_path()) + } else { + Err( + WorkspaceResolvePkgJsonFolderErrorKind::VersionNotSatisfied( + version_req.clone(), + version, + ) + .into(), + ) + } + } else { + // just match it + Ok(pkg_json.dir_path()) + } + } + RangeSetOrTag::Tag(_) => { + // always match tags + Ok(pkg_json.dir_path()) + } + } + } + PackageJsonDepWorkspaceReq::Tilde | PackageJsonDepWorkspaceReq::Caret => { + // always match tilde and caret requirements + Ok(pkg_json.dir_path()) + } + } + } + + pub fn pkg_json_dep_resolution(&self) -> PackageJsonDepResolution { + self.pkg_json_dep_resolution + } + + pub fn sloppy_imports_enabled(&self) -> bool { + match self.sloppy_imports_options { + SloppyImportsOptions::Enabled => true, + SloppyImportsOptions::Disabled => false, + } + } + + pub fn has_compiler_options_root_dirs(&self) -> bool { + !self + .compiler_options_root_dirs_resolver + .root_dirs_from_root + .is_empty() + || self + .compiler_options_root_dirs_resolver + .root_dirs_by_member + .values() + .flatten() + .any(|r| !r.is_empty()) + } +} + +#[derive(Deserialize, Serialize)] +pub struct SerializedWorkspaceResolverImportMap<'a> { + #[serde(borrow)] + pub specifier: Cow<'a, str>, + #[serde(borrow)] + pub json: Cow<'a, str>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializedResolverWorkspaceJsrPackage<'a> { + #[serde(borrow)] + pub relative_base: Cow<'a, str>, + #[serde(borrow)] + pub name: Cow<'a, str>, + pub version: Cow<'a, Option>, + pub exports: Cow<'a, IndexMap>, +} + +#[derive(Deserialize, Serialize)] +pub struct SerializableWorkspaceResolver<'a> { + #[serde(borrow)] + pub import_map: Option>, + #[serde(borrow)] + pub jsr_pkgs: Vec>, + pub package_jsons: Vec<(String, serde_json::Value)>, + pub pkg_json_resolution: PackageJsonDepResolution, + pub sloppy_imports_options: SloppyImportsOptions, + pub fs_cache_options: FsCacheOptions, + pub root_dirs_from_root: Vec>, + pub root_dirs_by_member: BTreeMap, Option>>>, +} + +#[derive(Debug, Clone, Copy)] +struct BaseUrl<'a>(&'a Url); + +impl BaseUrl<'_> { + fn make_relative_if_descendant<'a>(&self, target: &'a Url) -> Cow<'a, str> { + if target.scheme() != "file" { + return Cow::Borrowed(target.as_str()); + } + + match self.0.make_relative(target) { + Some(relative) => { + if relative.starts_with("../") { + Cow::Borrowed(target.as_str()) + } else { + Cow::Owned(relative) + } + } + None => Cow::Borrowed(target.as_str()), + } + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + use std::path::PathBuf; + + use deno_config::workspace::WorkspaceDirectory; + use deno_config::workspace::WorkspaceDiscoverOptions; + use deno_config::workspace::WorkspaceDiscoverStart; + use deno_path_util::url_from_directory_path; + use deno_path_util::url_from_file_path; + use deno_semver::VersionReq; + use serde_json::json; + use sys_traits::impls::InMemorySys; + use sys_traits::FsCanonicalize; + use url::Url; + + use super::*; + + pub struct UnreachableSys; + + impl sys_traits::BaseFsMetadata for UnreachableSys { + type Metadata = sys_traits::impls::RealFsMetadata; + + #[doc(hidden)] + fn base_fs_metadata( + &self, + _path: &Path, + ) -> std::io::Result { + unreachable!() + } + + #[doc(hidden)] + fn base_fs_symlink_metadata( + &self, + _path: &Path, + ) -> std::io::Result { + unreachable!() + } + } + + impl sys_traits::BaseFsRead for UnreachableSys { + fn base_fs_read( + &self, + _path: &Path, + ) -> std::io::Result> { + unreachable!() + } + } + + fn root_dir() -> PathBuf { + if cfg!(windows) { + PathBuf::from("C:\\Users\\user") + } else { + PathBuf::from("/home/user") + } + } + + #[test] + fn pkg_json_resolution() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "workspace": [ + "a", + "b", + "c", + ] + }), + ); + sys.fs_insert_json( + root_dir().join("a/deno.json"), + json!({ + "imports": { + "b": "./index.js", + }, + }), + ); + sys.fs_insert_json( + root_dir().join("b/package.json"), + json!({ + "dependencies": { + "pkg": "npm:pkg@^1.0.0", + }, + }), + ); + sys.fs_insert_json( + root_dir().join("c/package.json"), + json!({ + "name": "pkg", + "version": "0.5.0" + }), + ); + let workspace = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace); + assert_eq!(resolver.diagnostics(), Vec::new()); + let resolve = |name: &str, referrer: &str| { + resolver.resolve( + name, + &url_from_file_path(&deno_path_util::normalize_path( + root_dir().join(referrer), + )) + .unwrap(), + ResolutionKind::Execution, + ) + }; + match resolve("pkg", "b/index.js").unwrap() { + MappedResolution::PackageJson { + alias, + sub_path, + dep_result, + .. + } => { + assert_eq!(alias, "pkg"); + assert_eq!(sub_path, None); + dep_result.as_ref().unwrap(); + } + value => unreachable!("{:?}", value), + } + match resolve("pkg/sub-path", "b/index.js").unwrap() { + MappedResolution::PackageJson { + alias, + sub_path, + dep_result, + .. + } => { + assert_eq!(alias, "pkg"); + assert_eq!(sub_path.unwrap(), "sub-path"); + dep_result.as_ref().unwrap(); + } + value => unreachable!("{:?}", value), + } + + // pkg is not a dependency in this folder, so it should resolve + // to the workspace member + match resolve("pkg", "index.js").unwrap() { + MappedResolution::WorkspaceNpmPackage { + pkg_name, + sub_path, + target_pkg_json, + } => { + assert_eq!(pkg_name, "pkg"); + assert_eq!(sub_path, None); + assert_eq!(target_pkg_json.dir_path(), root_dir().join("c")); + } + _ => unreachable!(), + } + match resolve("pkg/sub-path", "index.js").unwrap() { + MappedResolution::WorkspaceNpmPackage { + pkg_name, + sub_path, + target_pkg_json, + } => { + assert_eq!(pkg_name, "pkg"); + assert_eq!(sub_path.unwrap(), "sub-path"); + assert_eq!(target_pkg_json.dir_path(), root_dir().join("c")); + } + _ => unreachable!(), + } + + // won't resolve the package outside the workspace + assert!(resolve("pkg", "../outside-workspace.js").is_err()); + } + + #[test] + fn single_pkg_no_import_map() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "name": "@scope/pkg", + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + let workspace = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace); + assert_eq!(resolver.diagnostics(), Vec::new()); + let result = resolver + .resolve( + "@scope/pkg", + &url_from_file_path(&root_dir().join("file.ts")).unwrap(), + ResolutionKind::Execution, + ) + .unwrap(); + match result { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!( + specifier, + url_from_file_path(&root_dir().join("mod.ts")).unwrap() + ); + } + _ => unreachable!(), + } + } + + #[test] + fn resolve_workspace_pkg_json_folder() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("package.json"), + json!({ + "workspaces": [ + "a", + "b", + "no-version" + ] + }), + ); + sys.fs_insert_json( + root_dir().join("a/package.json"), + json!({ + "name": "@scope/a", + "version": "1.0.0", + }), + ); + sys.fs_insert_json( + root_dir().join("b/package.json"), + json!({ + "name": "@scope/b", + "version": "2.0.0", + }), + ); + sys.fs_insert_json( + root_dir().join("no-version/package.json"), + json!({ + "name": "@scope/no-version", + }), + ); + let workspace = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace); + // resolve for pkg json dep + { + let resolve = |name: &str, req: &str| { + resolver.resolve_workspace_pkg_json_folder_for_pkg_json_dep( + name, + &PackageJsonDepWorkspaceReq::VersionReq( + VersionReq::parse_from_npm(req).unwrap(), + ), + ) + }; + assert_eq!( + resolve("non-existent", "*").map_err(|e| e.into_kind()), + Err(WorkspaceResolvePkgJsonFolderErrorKind::NotFound( + "non-existent".to_string() + )) + ); + assert_eq!( + resolve("@scope/a", "6").map_err(|e| e.into_kind()), + Err(WorkspaceResolvePkgJsonFolderErrorKind::VersionNotSatisfied( + VersionReq::parse_from_npm("6").unwrap(), + Version::parse_from_npm("1.0.0").unwrap(), + )) + ); + assert_eq!(resolve("@scope/a", "1").unwrap(), root_dir().join("a")); + assert_eq!(resolve("@scope/a", "*").unwrap(), root_dir().join("a")); + assert_eq!( + resolve("@scope/a", "workspace").unwrap(), + root_dir().join("a") + ); + assert_eq!(resolve("@scope/b", "2").unwrap(), root_dir().join("b")); + // just match any tags with the workspace + assert_eq!(resolve("@scope/a", "latest").unwrap(), root_dir().join("a")); + + // match any version for a pkg with no version + assert_eq!( + resolve("@scope/no-version", "1").unwrap(), + root_dir().join("no-version") + ); + assert_eq!( + resolve("@scope/no-version", "20").unwrap(), + root_dir().join("no-version") + ); + } + // resolve for specifier + { + let resolve = |pkg_req: &str| { + resolver.resolve_workspace_pkg_json_folder_for_npm_specifier( + &PackageReq::from_str(pkg_req).unwrap(), + ) + }; + assert_eq!(resolve("non-existent@*"), None); + assert_eq!( + resolve("@scope/no-version@1").unwrap(), + root_dir().join("no-version") + ); + + // won't match for tags + assert_eq!(resolve("@scope/a@workspace"), None); + assert_eq!(resolve("@scope/a@latest"), None); + } + } + + #[test] + fn resolve_workspace_pkg_json_workspace_deno_json_import_map() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("package.json"), + json!({ + "workspaces": ["*"] + }), + ); + sys.fs_insert_json( + root_dir().join("a/package.json"), + json!({ + "name": "@scope/a", + "version": "1.0.0", + }), + ); + sys.fs_insert_json( + root_dir().join("a/deno.json"), + json!({ + "name": "@scope/jsr-pkg", + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + + let workspace = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace); + { + let resolution = resolver + .resolve( + "@scope/jsr-pkg", + &url_from_file_path(&root_dir().join("b.ts")).unwrap(), + ResolutionKind::Execution, + ) + .unwrap(); + match resolution { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!( + specifier, + url_from_file_path(&root_dir().join("a/mod.ts")).unwrap() + ); + } + _ => unreachable!(), + } + } + { + let resolution_err = resolver + .resolve( + "@scope/jsr-pkg/not-found-export", + &url_from_file_path(&root_dir().join("b.ts")).unwrap(), + ResolutionKind::Execution, + ) + .unwrap_err(); + match resolution_err { + MappedResolutionError::Workspace( + WorkspaceResolveError::UnknownExport { + package_name, + export_name, + exports, + }, + ) => { + assert_eq!(package_name, "@scope/jsr-pkg"); + assert_eq!(export_name, "./not-found-export"); + assert_eq!(exports, vec!["."]); + } + _ => unreachable!(), + } + } + } + + #[test] + fn root_member_imports_and_scopes() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "workspace": ["member"], + "imports": { + "@scope/pkg": "jsr:@scope/pkg@1", + }, + "scopes": { + "https://deno.land/x/": { + "@scope/pkg": "jsr:@scope/pkg@2", + }, + }, + }), + ); + // Overrides `rootDirs` from workspace root. + sys.fs_insert_json( + root_dir().join("member/deno.json"), + json!({ + "imports": { + "@scope/pkg": "jsr:@scope/pkg@3", + }, + // will ignore this scopes because it's not in the root + "scopes": { + "https://deno.land/x/other": { + "@scope/pkg": "jsr:@scope/pkg@4", + }, + }, + }), + ); + + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = WorkspaceResolver::from_workspace( + &workspace_dir.workspace, + sys.clone(), + super::CreateResolverOptions { + pkg_json_dep_resolution: PackageJsonDepResolution::Enabled, + specified_import_map: None, + sloppy_imports_options: SloppyImportsOptions::Disabled, + fs_cache_options: FsCacheOptions::Enabled, + }, + ) + .unwrap(); + assert_eq!( + serde_json::from_str::( + &resolver.maybe_import_map().unwrap().to_json() + ) + .unwrap(), + json!({ + "imports": { + "@scope/pkg": "jsr:@scope/pkg@1", + "@scope/pkg/": "jsr:/@scope/pkg@1/", + }, + "scopes": { + "https://deno.land/x/": { + "@scope/pkg": "jsr:@scope/pkg@2", + "@scope/pkg/": "jsr:/@scope/pkg@2/", + }, + "./member/": { + "@scope/pkg": "jsr:@scope/pkg@3", + "@scope/pkg/": "jsr:/@scope/pkg@3/", + }, + }, + }), + ); + } + + #[test] + fn resolve_sloppy_imports() { + let sys = InMemorySys::default(); + let root_url = url_from_file_path( + &sys_traits::impls::RealSys.fs_canonicalize("/").unwrap(), + ) + .unwrap(); + let fs = CachedMetadataFs::new(sys.clone(), FsCacheOptions::Enabled); + let sloppy_imports_resolver = + SloppyImportsResolver::new(fs, SloppyImportsOptions::Enabled); + + // scenarios like resolving ./example.js to ./example.ts + for (file_from, file_to) in [ + ("file1.js", "file1.ts"), + ("file2.js", "file2.tsx"), + ("file3.mjs", "file3.mts"), + ] { + let specifier = root_url.join(file_to).unwrap(); + sys.fs_insert(url_to_file_path(&specifier).unwrap(), ""); + let sloppy_specifier = root_url.join(file_from).unwrap(); + assert_eq!( + sloppy_imports_resolver.resolve(&specifier, ResolutionKind::Execution), + None, + ); + assert_eq!( + sloppy_imports_resolver + .resolve(&sloppy_specifier, ResolutionKind::Execution), + Some((specifier, SloppyImportsResolutionReason::JsToTs)), + ); + } + + // no extension scenarios + for file in [ + "file10.js", + "file11.ts", + "file12.js", + "file13.tsx", + "file14.jsx", + "file15.mjs", + "file16.mts", + ] { + let specifier = root_url.join(file).unwrap(); + sys.fs_insert(url_to_file_path(&specifier).unwrap(), ""); + let sloppy_specifier = + root_url.join(file.split_once('.').unwrap().0).unwrap(); + assert_eq!( + sloppy_imports_resolver.resolve(&specifier, ResolutionKind::Execution), + None, + ); + assert_eq!( + sloppy_imports_resolver + .resolve(&sloppy_specifier, ResolutionKind::Execution), + Some((specifier, SloppyImportsResolutionReason::NoExtension)), + ); + } + + // .ts and .js exists, .js specified (goes to specified) + { + let ts_specifier = root_url.join("ts_and_js.ts").unwrap(); + sys.fs_insert(url_to_file_path(&ts_specifier).unwrap(), ""); + let js_specifier = root_url.join("ts_and_js.js").unwrap(); + sys.fs_insert(url_to_file_path(&js_specifier).unwrap(), ""); + assert_eq!( + sloppy_imports_resolver + .resolve(&js_specifier, ResolutionKind::Execution), + None, + ); + } + + // only js exists, .js specified + { + let specifier = root_url.join("js_only.js").unwrap(); + sys.fs_insert(url_to_file_path(&specifier).unwrap(), ""); + assert_eq!( + sloppy_imports_resolver.resolve(&specifier, ResolutionKind::Execution), + None, + ); + assert_eq!( + sloppy_imports_resolver.resolve(&specifier, ResolutionKind::Types), + None, + ); + } + + // resolving a directory to an index file + { + let specifier = root_url.join("routes/index.ts").unwrap(); + sys.fs_insert(url_to_file_path(&specifier).unwrap(), ""); + let sloppy_specifier = root_url.join("routes").unwrap(); + assert_eq!( + sloppy_imports_resolver + .resolve(&sloppy_specifier, ResolutionKind::Execution), + Some((specifier, SloppyImportsResolutionReason::Directory)), + ); + } + + // both a directory and a file with specifier is present + { + let specifier = root_url.join("api.ts").unwrap(); + sys.fs_insert(url_to_file_path(&specifier).unwrap(), ""); + let bar_specifier = root_url.join("api/bar.ts").unwrap(); + sys.fs_insert(url_to_file_path(&bar_specifier).unwrap(), ""); + let sloppy_specifier = root_url.join("api").unwrap(); + assert_eq!( + sloppy_imports_resolver + .resolve(&sloppy_specifier, ResolutionKind::Execution), + Some((specifier, SloppyImportsResolutionReason::NoExtension)), + ); + } + } + + #[test] + fn test_sloppy_import_resolution_suggestion_message() { + // directory + assert_eq!( + SloppyImportsResolutionReason::Directory + .suggestion_message_for_specifier( + &Url::parse("file:///dir/index.js").unwrap() + ) + .as_str(), + "Maybe specify path to 'index.js' file in directory instead" + ); + // no ext + assert_eq!( + SloppyImportsResolutionReason::NoExtension + .suggestion_message_for_specifier( + &Url::parse("file:///dir/index.mjs").unwrap() + ) + .as_str(), + "Maybe add a '.mjs' extension" + ); + // js to ts + assert_eq!( + SloppyImportsResolutionReason::JsToTs + .suggestion_message_for_specifier( + &Url::parse("file:///dir/index.mts").unwrap() + ) + .as_str(), + "Maybe change the extension to '.mts'" + ); + } + + #[test] + fn resolve_compiler_options_root_dirs() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "workspace": ["member", "member2"], + "compilerOptions": { + "rootDirs": ["member", "member2", "member2_types"], + }, + }), + ); + // Overrides `rootDirs` from workspace root. + sys.fs_insert_json( + root_dir().join("member/deno.json"), + json!({ + "compilerOptions": { + "rootDirs": ["foo", "foo_types"], + }, + }), + ); + // Use `rootDirs` from workspace root. + sys.fs_insert_json(root_dir().join("member2/deno.json"), json!({})); + sys.fs_insert(root_dir().join("member/foo_types/import.ts"), ""); + sys.fs_insert(root_dir().join("member2_types/import.ts"), ""); + // This file should be ignored. It would be used if `member/deno.json` had + // no `rootDirs`. + sys.fs_insert(root_dir().join("member2_types/foo/import.ts"), ""); + + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = WorkspaceResolver::from_workspace( + &workspace_dir.workspace, + sys.clone(), + super::CreateResolverOptions { + pkg_json_dep_resolution: PackageJsonDepResolution::Enabled, + specified_import_map: None, + sloppy_imports_options: SloppyImportsOptions::Disabled, + fs_cache_options: FsCacheOptions::Enabled, + }, + ) + .unwrap(); + let root_dir_url = workspace_dir.workspace.root_dir(); + + let referrer = root_dir_url.join("member/foo/mod.ts").unwrap(); + let resolution = resolver + .resolve("./import.ts", &referrer, ResolutionKind::Types) + .unwrap(); + let MappedResolution::Normal { + specifier, + sloppy_reason, + used_compiler_options_root_dirs, + .. + } = &resolution + else { + unreachable!("{:#?}", &resolution); + }; + assert_eq!( + specifier.as_str(), + root_dir_url + .join("member/foo_types/import.ts") + .unwrap() + .as_str() + ); + assert_eq!(sloppy_reason, &None); + assert!(used_compiler_options_root_dirs); + + let referrer = root_dir_url.join("member2/mod.ts").unwrap(); + let resolution = resolver + .resolve("./import.ts", &referrer, ResolutionKind::Types) + .unwrap(); + let MappedResolution::Normal { + specifier, + sloppy_reason, + used_compiler_options_root_dirs, + .. + } = &resolution + else { + unreachable!("{:#?}", &resolution); + }; + assert_eq!( + specifier.as_str(), + root_dir_url + .join("member2_types/import.ts") + .unwrap() + .as_str() + ); + assert_eq!(sloppy_reason, &None); + assert!(used_compiler_options_root_dirs); + + // Ignore rootDirs for `ResolutionKind::Execution`. + let referrer = root_dir_url.join("member/foo/mod.ts").unwrap(); + let resolution = resolver + .resolve("./import.ts", &referrer, ResolutionKind::Execution) + .unwrap(); + let MappedResolution::Normal { + specifier, + sloppy_reason, + used_compiler_options_root_dirs, + .. + } = &resolution + else { + unreachable!("{:#?}", &resolution); + }; + assert_eq!( + specifier.as_str(), + root_dir_url.join("member/foo/import.ts").unwrap().as_str() + ); + assert_eq!(sloppy_reason, &None); + assert!(!used_compiler_options_root_dirs); + + // Ignore rootDirs for `ResolutionKind::Execution`. + let referrer = root_dir_url.join("member2/mod.ts").unwrap(); + let resolution = resolver + .resolve("./import.ts", &referrer, ResolutionKind::Execution) + .unwrap(); + let MappedResolution::Normal { + specifier, + sloppy_reason, + used_compiler_options_root_dirs, + .. + } = &resolution + else { + unreachable!("{:#?}", &resolution); + }; + assert_eq!( + specifier.as_str(), + root_dir_url.join("member2/import.ts").unwrap().as_str() + ); + assert_eq!(sloppy_reason, &None); + assert!(!used_compiler_options_root_dirs); + } + + #[test] + fn resolve_compiler_options_root_dirs_and_sloppy_imports() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "compilerOptions": { + "rootDirs": ["subdir", "subdir_types"], + }, + }), + ); + sys.fs_insert(root_dir().join("subdir_types/import.ts"), ""); + + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = WorkspaceResolver::from_workspace( + &workspace_dir.workspace, + sys.clone(), + super::CreateResolverOptions { + pkg_json_dep_resolution: PackageJsonDepResolution::Enabled, + specified_import_map: None, + sloppy_imports_options: SloppyImportsOptions::Enabled, + fs_cache_options: FsCacheOptions::Enabled, + }, + ) + .unwrap(); + let root_dir_url = workspace_dir.workspace.root_dir(); + + let referrer = root_dir_url.join("subdir/mod.ts").unwrap(); + let resolution = resolver + .resolve("./import", &referrer, ResolutionKind::Types) + .unwrap(); + let MappedResolution::Normal { + specifier, + sloppy_reason, + used_compiler_options_root_dirs, + .. + } = &resolution + else { + unreachable!("{:#?}", &resolution); + }; + assert_eq!( + specifier.as_str(), + root_dir_url + .join("subdir_types/import.ts") + .unwrap() + .as_str() + ); + assert_eq!( + sloppy_reason, + &Some(SloppyImportsResolutionReason::NoExtension) + ); + assert!(used_compiler_options_root_dirs); + } + + #[test] + fn specified_import_map() { + let sys = InMemorySys::default(); + sys.fs_insert_json(root_dir().join("deno.json"), json!({})); + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = WorkspaceResolver::from_workspace( + &workspace_dir.workspace, + sys, + super::CreateResolverOptions { + pkg_json_dep_resolution: PackageJsonDepResolution::Enabled, + specified_import_map: Some(SpecifiedImportMap { + base_url: url_from_directory_path(&root_dir()).unwrap(), + value: json!({ + "imports": { + "b": "./b/mod.ts", + }, + }), + }), + sloppy_imports_options: SloppyImportsOptions::Disabled, + fs_cache_options: FsCacheOptions::Enabled, + }, + ) + .unwrap(); + let root = url_from_directory_path(&root_dir()).unwrap(); + match resolver + .resolve( + "b", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::Normal { specifier, .. } => { + assert_eq!(specifier, root.join("b/mod.ts").unwrap()); + } + _ => unreachable!(), + } + } + + #[test] + fn workspace_specified_import_map() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "workspace": ["./a"] + }), + ); + sys.fs_insert_json(root_dir().join("a").join("deno.json"), json!({})); + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + WorkspaceResolver::from_workspace( + &workspace_dir.workspace, + UnreachableSys, + super::CreateResolverOptions { + pkg_json_dep_resolution: PackageJsonDepResolution::Enabled, + specified_import_map: Some(SpecifiedImportMap { + base_url: url_from_directory_path(&root_dir()).unwrap(), + value: json!({ + "imports": { + "b": "./b/mod.ts", + }, + }), + }), + sloppy_imports_options: SloppyImportsOptions::Disabled, + fs_cache_options: FsCacheOptions::Enabled, + }, + ) + .unwrap(); + } + + #[test] + fn resolves_patch_member_with_version() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "patch": ["../patch"] + }), + ); + sys.fs_insert_json( + root_dir().join("../patch/deno.json"), + json!({ + "name": "@scope/patch", + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace_dir); + let root = url_from_directory_path(&root_dir()).unwrap(); + match resolver + .resolve( + "@scope/patch", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!(specifier, root.join("../patch/mod.ts").unwrap()); + } + _ => unreachable!(), + } + // matching version + match resolver + .resolve( + "jsr:@scope/patch@1", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!(specifier, root.join("../patch/mod.ts").unwrap()); + } + _ => unreachable!(), + } + // not matching version + match resolver + .resolve( + "jsr:@scope/patch@2", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::Normal { + specifier, + maybe_diagnostic, + .. + } => { + assert_eq!(specifier, Url::parse("jsr:@scope/patch@2").unwrap()); + assert_eq!( + maybe_diagnostic, + Some(Box::new( + MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { + is_patch: true, + reference: JsrPackageReqReference::from_str("jsr:@scope/patch@2") + .unwrap(), + local_version: Version::parse_from_npm("1.0.0").unwrap(), + } + )) + ); + } + _ => unreachable!(), + } + } + + #[test] + fn resolves_patch_member_no_version() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "patch": ["../patch"] + }), + ); + sys.fs_insert_json( + root_dir().join("../patch/deno.json"), + json!({ + "name": "@scope/patch", + "exports": "./mod.ts" + }), + ); + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace_dir); + let root = url_from_directory_path(&root_dir()).unwrap(); + match resolver + .resolve( + "@scope/patch", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!(specifier, root.join("../patch/mod.ts").unwrap()); + } + _ => unreachable!(), + } + // always resolves, no matter what version + match resolver + .resolve( + "jsr:@scope/patch@12", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!(specifier, root.join("../patch/mod.ts").unwrap()); + } + _ => unreachable!(), + } + } + + #[test] + fn resolves_workspace_member() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "workspace": ["./member"] + }), + ); + sys.fs_insert_json( + root_dir().join("./member/deno.json"), + json!({ + "name": "@scope/member", + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace_dir); + let root = url_from_directory_path(&root_dir()).unwrap(); + match resolver + .resolve( + "@scope/member", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!(specifier, root.join("./member/mod.ts").unwrap()); + } + _ => unreachable!(), + } + // matching version + match resolver + .resolve( + "jsr:@scope/member@1", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!(specifier, root.join("./member/mod.ts").unwrap()); + } + _ => unreachable!(), + } + // not matching version + match resolver + .resolve( + "jsr:@scope/member@2", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::Normal { + specifier, + maybe_diagnostic, + .. + } => { + assert_eq!(specifier, Url::parse("jsr:@scope/member@2").unwrap()); + assert_eq!( + maybe_diagnostic, + Some(Box::new( + MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { + is_patch: false, + reference: JsrPackageReqReference::from_str( + "jsr:@scope/member@2" + ) + .unwrap(), + local_version: Version::parse_from_npm("1.0.0").unwrap(), + } + )) + ); + } + _ => unreachable!(), + } + } + + #[test] + fn resolves_patch_workspace() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "imports": { + "@std/fs": "jsr:@std/fs@0.200.0" + }, + "patch": ["../patch"] + }), + ); + sys.fs_insert_json( + root_dir().join("../patch/deno.json"), + json!({ + "workspace": ["./member"] + }), + ); + sys.fs_insert_json( + root_dir().join("../patch/member/deno.json"), + json!({ + "name": "@scope/patch", + "version": "1.0.0", + "exports": "./mod.ts", + "imports": { + "@std/fs": "jsr:@std/fs@1" + } + }), + ); + let workspace_dir = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace_dir); + let root = url_from_directory_path(&root_dir()).unwrap(); + match resolver + .resolve( + "jsr:@scope/patch@1", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + assert_eq!(specifier, root.join("../patch/member/mod.ts").unwrap()); + } + _ => unreachable!(), + } + // resolving @std/fs from root + match resolver + .resolve( + "@std/fs", + &root.join("main.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::Normal { specifier, .. } => { + assert_eq!(specifier, Url::parse("jsr:@std/fs@0.200.0").unwrap()); + } + _ => unreachable!(), + } + // resolving @std/fs in patched package + match resolver + .resolve( + "@std/fs", + &root.join("../patch/member/mod.ts").unwrap(), + ResolutionKind::Execution, + ) + .unwrap() + { + MappedResolution::Normal { specifier, .. } => { + assert_eq!(specifier, Url::parse("jsr:@std/fs@1").unwrap()); + } + _ => unreachable!(), + } + } + + #[test] + fn invalid_package_name_with_slashes() { + let sys = InMemorySys::default(); + sys.fs_insert_json( + root_dir().join("deno.json"), + json!({ + "workspace": ["./libs/math"] + }), + ); + sys.fs_insert_json( + root_dir().join("libs/math/deno.json"), + json!({ + "name": "@deno-test/libs/math", // Invalid package name containing slashes + "version": "1.0.0", + "exports": "./mod.ts" + }), + ); + let workspace = workspace_at_start_dir(&sys, &root_dir()); + let resolver = create_resolver(&workspace); + let result = resolver.resolve( + "@deno-test/libs/math", + &url_from_file_path(&root_dir().join("main.ts")).unwrap(), + ResolutionKind::Execution, + ); + // Resolve shouldn't panic and tt should result in unmapped + // bare specifier error as the package name is invalid. + assert!(result.err().unwrap().is_unmapped_bare_specifier()); + + let diagnostics = workspace.workspace.diagnostics(); + assert_eq!(diagnostics.len(), 1); + assert!(diagnostics + .first() + .unwrap() + .to_string() + .starts_with(r#"Invalid workspace member name "@deno-test/libs/math"."#)); + } + + fn create_resolver( + workspace_dir: &WorkspaceDirectory, + ) -> WorkspaceResolver { + WorkspaceResolver::from_workspace( + &workspace_dir.workspace, + UnreachableSys, + super::CreateResolverOptions { + pkg_json_dep_resolution: PackageJsonDepResolution::Enabled, + specified_import_map: None, + sloppy_imports_options: SloppyImportsOptions::Disabled, + fs_cache_options: FsCacheOptions::Enabled, + }, + ) + .unwrap() + } + + fn workspace_at_start_dir( + sys: &InMemorySys, + start_dir: &Path, + ) -> WorkspaceDirectory { + WorkspaceDirectory::discover( + sys, + WorkspaceDiscoverStart::Paths(&[start_dir.to_path_buf()]), + &WorkspaceDiscoverOptions { + discover_pkg_json: true, + ..Default::default() + }, + ) + .unwrap() + } +} diff --git a/resolvers/node/resolution.rs b/resolvers/node/resolution.rs index 495c71bc8383ce..4ed7f9229bd8a3 100644 --- a/resolvers/node/resolution.rs +++ b/resolvers/node/resolution.rs @@ -110,15 +110,6 @@ pub enum NodeResolutionKind { Types, } -impl From for deno_config::workspace::ResolutionKind { - fn from(value: NodeResolutionKind) -> Self { - match value { - NodeResolutionKind::Execution => Self::Execution, - NodeResolutionKind::Types => Self::Types, - } - } -} - impl NodeResolutionKind { pub fn is_types(&self) -> bool { matches!(self, NodeResolutionKind::Types) diff --git a/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/__test__.jsonc b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/__test__.jsonc new file mode 100644 index 00000000000000..bf7e7e1c94c814 --- /dev/null +++ b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/__test__.jsonc @@ -0,0 +1,4 @@ +{ + "args": "check --quiet subdir/mod.ts", + "output": "" +} diff --git a/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/deno.json b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/deno.json new file mode 100644 index 00000000000000..d2148a5bb5be73 --- /dev/null +++ b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/deno.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "rootDirs": ["subdir", "subdir_types"] + }, + "unstable": ["sloppy-imports"] +} diff --git a/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir/mod.ts b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir/mod.ts new file mode 100644 index 00000000000000..1de59ebda0eca9 --- /dev/null +++ b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir/mod.ts @@ -0,0 +1,3 @@ +import type { someType } from "./import"; +const foo: someType = ""; +console.log(foo); diff --git a/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir_types/import.ts b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir_types/import.ts new file mode 100644 index 00000000000000..423236068f24f4 --- /dev/null +++ b/tests/specs/check/compiler_options_root_dirs_and_sloppy_imports/subdir_types/import.ts @@ -0,0 +1 @@ +export type someType = string;