diff options
Diffstat (limited to 'cli/lsp/config.rs')
-rw-r--r-- | cli/lsp/config.rs | 1168 |
1 files changed, 634 insertions, 534 deletions
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 861a63d0c..5b549fc39 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1,22 +1,27 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use super::logging::lsp_log; -use crate::args::discover_npmrc; -use crate::args::CliLockfile; -use crate::args::ConfigFile; -use crate::args::FmtOptions; -use crate::args::LintOptions; -use crate::args::DENO_FUTURE; -use crate::cache::FastInsecureHasher; -use crate::file_fetcher::FileFetcher; -use crate::lsp::logging::lsp_warn; -use crate::tools::lint::get_configured_rules; -use crate::tools::lint::ConfiguredRules; -use crate::util::fs::canonicalize_path_maybe_not_exists; use deno_ast::MediaType; +use deno_config::fs::DenoConfigFs; +use deno_config::fs::RealDenoConfigFs; +use deno_config::glob::FilePatterns; +use deno_config::glob::PathOrPatternSet; +use deno_config::package_json::PackageJsonCache; +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::WorkspaceDiscoverOptions; +use deno_config::workspace::WorkspaceEmptyOptions; +use deno_config::workspace::WorkspaceMemberContext; +use deno_config::workspace::WorkspaceResolver; +use deno_config::DenoJsonCache; +use deno_config::FmtConfig; use deno_config::FmtOptionsConfig; use deno_config::TsConfig; -use deno_core::normalize_path; +use deno_core::anyhow::anyhow; +use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; use deno_core::serde::de::DeserializeOwned; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; @@ -24,25 +29,33 @@ use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; use deno_core::ModuleSpecifier; -use deno_lint::linter::LintConfig; use deno_npm::npm_rc::ResolvedNpmRc; -use deno_runtime::deno_fs::DenoConfigFsAdapter; -use deno_runtime::deno_fs::RealFs; use deno_runtime::deno_node::PackageJson; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::fs_util::specifier_to_file_path; -use deno_semver::package::PackageNv; -use deno_semver::Version; -use import_map::ImportMap; use indexmap::IndexSet; use lsp::Url; use lsp_types::ClientCapabilities; use std::collections::BTreeMap; use std::collections::HashMap; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use tower_lsp::lsp_types as lsp; +use super::logging::lsp_log; +use crate::args::discover_npmrc_from_workspace; +use crate::args::has_flag_env_var; +use crate::args::CliLockfile; +use crate::args::ConfigFile; +use crate::args::DENO_FUTURE; +use crate::cache::FastInsecureHasher; +use crate::file_fetcher::FileFetcher; +use crate::lsp::logging::lsp_warn; +use crate::tools::lint::get_configured_rules; +use crate::tools::lint::ConfiguredRules; +use crate::util::fs::canonicalize_path_maybe_not_exists; + pub const SETTINGS_SECTION: &str = "deno"; fn is_true() -> bool { @@ -934,10 +947,10 @@ impl Config { } pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { - let config_file = self.tree.config_file_for_specifier(specifier); - if let Some(cf) = config_file { - if let Ok(files) = cf.to_exclude_files_config() { - if !files.matches_specifier(specifier) { + let data = self.tree.data_for_specifier(specifier); + if let Some(data) = &data { + if let Ok(path) = specifier.to_file_path() { + if data.exclude_files.matches_path(&path) { return false; } } @@ -945,18 +958,16 @@ impl Config { self .settings .specifier_enabled(specifier) - .unwrap_or_else(|| config_file.is_some()) + .unwrap_or_else(|| data.and_then(|d| d.maybe_deno_json()).is_some()) } pub fn specifier_enabled_for_test( &self, specifier: &ModuleSpecifier, ) -> bool { - if let Some(cf) = self.tree.config_file_for_specifier(specifier) { - if let Ok(options) = cf.to_test_config() { - if !options.files.matches_specifier(specifier) { - return false; - } + if let Some(data) = self.tree.data_for_specifier(specifier) { + if !data.test_config.files.matches_specifier(specifier) { + return false; } } self.specifier_enabled(specifier) @@ -1083,16 +1094,11 @@ impl LspTsConfig { } } -#[derive(Debug, Clone)] -pub struct LspPackageConfig { - pub nv: PackageNv, - pub exports: Value, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigWatchedFileType { DenoJson, Lockfile, + NpmRc, PackageJson, ImportMap, } @@ -1100,495 +1106,455 @@ pub enum ConfigWatchedFileType { /// Contains the config file and dependent information. #[derive(Debug, Clone)] pub struct ConfigData { - pub scope: ModuleSpecifier, - pub config_file: Option<Arc<ConfigFile>>, - pub fmt_options: Arc<FmtOptions>, - pub lint_options: Arc<LintOptions>, - pub lint_config: LintConfig, + pub scope: Arc<ModuleSpecifier>, + pub workspace: Arc<Workspace>, + pub member_ctx: Arc<WorkspaceMemberContext>, + pub fmt_config: Arc<deno_config::FmtConfig>, + pub lint_config: Arc<deno_config::LintConfig>, + pub test_config: Arc<deno_config::TestConfig>, + pub exclude_files: Arc<PathOrPatternSet>, + pub deno_lint_config: deno_lint::linter::LintConfig, pub lint_rules: Arc<ConfiguredRules>, pub ts_config: Arc<LspTsConfig>, pub byonm: bool, pub node_modules_dir: Option<PathBuf>, pub vendor_dir: Option<PathBuf>, pub lockfile: Option<Arc<CliLockfile>>, - pub package_json: Option<Arc<PackageJson>>, pub npmrc: Option<Arc<ResolvedNpmRc>>, - pub import_map: Option<Arc<ImportMap>>, - pub import_map_from_settings: bool, - pub package_config: Option<Arc<LspPackageConfig>>, - pub is_workspace_root: bool, - pub workspace_root_dir: ModuleSpecifier, - /// Workspace member directories. For a workspace root this will be a list of - /// members. For a member this will be the same list, representing self and - /// siblings. For a solitary package this will be `vec![self.scope]`. These - /// are the list of packages to override with local resolutions for this - /// config scope. - pub workspace_members: Arc<Vec<ModuleSpecifier>>, + pub resolver: Arc<WorkspaceResolver>, + pub import_map_from_settings: Option<ModuleSpecifier>, watched_files: HashMap<ModuleSpecifier, ConfigWatchedFileType>, } impl ConfigData { async fn load( - config_file_specifier: Option<&ModuleSpecifier>, + specified_config: Option<&Path>, scope: &ModuleSpecifier, - workspace_root: Option<&ConfigData>, settings: &Settings, - file_fetcher: Option<&Arc<FileFetcher>>, + file_fetcher: &Arc<FileFetcher>, + // sync requirement is because the lsp requires sync + cached_deno_config_fs: &(dyn DenoConfigFs + Sync), + deno_json_cache: &(dyn DenoJsonCache + Sync), + pkg_json_cache: &(dyn PackageJsonCache + Sync), ) -> Self { - if let Some(specifier) = config_file_specifier { - match ConfigFile::from_specifier( - &DenoConfigFsAdapter::new(&RealFs), - specifier.clone(), - &deno_config::ConfigParseOptions::default(), - ) { - Ok(config_file) => { - lsp_log!( - " Resolved Deno configuration file: \"{}\"", - config_file.specifier.as_str() - ); - Self::load_inner( - Some(config_file), - scope, - workspace_root, - settings, - file_fetcher, - ) - .await - } - Err(err) => { - lsp_warn!( - " Couldn't read Deno configuration file \"{}\": {}", - specifier.as_str(), - err - ); - let mut data = Self::load_inner( - None, - scope, - workspace_root, - settings, - file_fetcher, - ) - .await; - data - .watched_files - .insert(specifier.clone(), ConfigWatchedFileType::DenoJson); - let canonicalized_specifier = specifier - .to_file_path() - .ok() - .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok()) - .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); - if let Some(specifier) = canonicalized_specifier { - data - .watched_files - .insert(specifier, ConfigWatchedFileType::DenoJson); + let scope = Arc::new(scope.clone()); + let discover_result = match scope.to_file_path() { + Ok(scope_dir_path) => { + let paths = [scope_dir_path]; + Workspace::discover( + match specified_config { + Some(config_path) => { + deno_config::workspace::WorkspaceDiscoverStart::ConfigFile( + config_path, + ) + } + None => { + deno_config::workspace::WorkspaceDiscoverStart::Paths(&paths) + } + }, + &WorkspaceDiscoverOptions { + fs: cached_deno_config_fs, + additional_config_file_names: &[], + deno_json_cache: Some(deno_json_cache), + pkg_json_cache: Some(pkg_json_cache), + discover_pkg_json: !has_flag_env_var("DENO_NO_PACKAGE_JSON"), + config_parse_options: Default::default(), + maybe_vendor_override: None, + }, + ) + .map(Arc::new) + .map_err(AnyError::from) + } + Err(()) => Err(anyhow!("Scope '{}' was not a directory path.", scope)), + }; + match discover_result { + Ok(workspace) => { + Self::load_inner(workspace, scope, settings, Some(file_fetcher)).await + } + Err(err) => { + lsp_warn!(" Couldn't open workspace \"{}\": {}", scope.as_str(), err); + let workspace = Arc::new(Workspace::empty(WorkspaceEmptyOptions { + root_dir: scope.clone(), + use_vendor_dir: VendorEnablement::Disable, + })); + let mut data = Self::load_inner( + workspace, + scope.clone(), + settings, + Some(file_fetcher), + ) + .await; + // check if any of these need to be added to the workspace + let files = [ + ( + scope.join("deno.json").unwrap(), + ConfigWatchedFileType::DenoJson, + ), + ( + scope.join("deno.jsonc").unwrap(), + ConfigWatchedFileType::DenoJson, + ), + ( + scope.join("package.json").unwrap(), + ConfigWatchedFileType::PackageJson, + ), + ]; + for (url, file_type) in files { + let Some(file_path) = url.to_file_path().ok() else { + continue; + }; + if file_path.exists() { + data.watched_files.insert(url.clone(), file_type); + let canonicalized_specifier = + canonicalize_path_maybe_not_exists(&file_path) + .ok() + .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); + if let Some(specifier) = canonicalized_specifier { + data.watched_files.insert(specifier, file_type); + } } - data } + data } - } else { - Self::load_inner(None, scope, workspace_root, settings, file_fetcher) - .await } } async fn load_inner( - config_file: Option<ConfigFile>, - scope: &ModuleSpecifier, - workspace_root: Option<&ConfigData>, + workspace: Arc<Workspace>, + scope: Arc<ModuleSpecifier>, settings: &Settings, file_fetcher: Option<&Arc<FileFetcher>>, ) -> Self { - let (settings, workspace_folder) = settings.get_for_specifier(scope); - let mut watched_files = HashMap::with_capacity(6); - if let Some(config_file) = &config_file { - watched_files - .entry(config_file.specifier.clone()) - .or_insert(ConfigWatchedFileType::DenoJson); - } - let config_file_path = config_file - .as_ref() - .and_then(|c| specifier_to_file_path(&c.specifier).ok()); - let config_file_canonicalized_specifier = config_file_path - .as_ref() - .and_then(|p| canonicalize_path_maybe_not_exists(p).ok()) - .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); - if let Some(specifier) = config_file_canonicalized_specifier { - watched_files - .entry(specifier) - .or_insert(ConfigWatchedFileType::DenoJson); - } + let (settings, workspace_folder) = settings.get_for_specifier(&scope); + let mut watched_files = HashMap::with_capacity(10); + let mut add_watched_file = + |specifier: ModuleSpecifier, file_type: ConfigWatchedFileType| { + let maybe_canonicalized = specifier + .to_file_path() + .ok() + .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok()) + .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); + if let Some(canonicalized) = maybe_canonicalized { + if canonicalized != specifier { + watched_files.entry(canonicalized).or_insert(file_type); + } + } + watched_files.entry(specifier).or_insert(file_type); + }; - let mut fmt_options = None; - if let Some(workspace_data) = workspace_root { - let has_own_fmt_options = config_file - .as_ref() - .is_some_and(|config_file| config_file.json.fmt.is_some()); - if !has_own_fmt_options { - fmt_options = Some(workspace_data.fmt_options.clone()) - } - } - let fmt_options = fmt_options.unwrap_or_else(|| { - config_file - .as_ref() - .and_then(|config_file| { - config_file - .to_fmt_config() - .map(|o| FmtOptions::resolve(o, &Default::default())) - .inspect_err(|err| { - lsp_warn!(" Couldn't read formatter configuration: {}", err) - }) - .ok() - }) - .map(Arc::new) - .unwrap_or_default() - }); + let member_ctx = workspace.resolve_start_ctx(); - let mut lint_options_rules = None; - if let Some(workspace_data) = workspace_root { - let has_own_lint_options = config_file - .as_ref() - .is_some_and(|config_file| config_file.json.lint.is_some()); - if !has_own_lint_options { - lint_options_rules = Some(( - workspace_data.lint_options.clone(), - workspace_data.lint_rules.clone(), - )) - } + if let Some(deno_json) = member_ctx.maybe_deno_json() { + lsp_log!( + " Resolved Deno configuration file: \"{}\"", + deno_json.specifier + ); + + add_watched_file( + deno_json.specifier.clone(), + ConfigWatchedFileType::DenoJson, + ); } - let (lint_options, lint_rules) = lint_options_rules.unwrap_or_else(|| { - let lint_options = config_file - .as_ref() - .and_then(|config_file| { - config_file - .to_lint_config() - .map(|o| LintOptions::resolve(o, &Default::default())) - .inspect_err(|err| { - lsp_warn!(" Couldn't read lint configuration: {}", err) - }) - .ok() - }) - .map(Arc::new) - .unwrap_or_default(); - let lint_rules = Arc::new(get_configured_rules( - lint_options.rules.clone(), - config_file.as_ref(), - )); - (lint_options, lint_rules) - }); - let ts_config = LspTsConfig::new(config_file.as_ref()); + if let Some(pkg_json) = member_ctx.maybe_pkg_json() { + lsp_log!(" Resolved package.json: \"{}\"", pkg_json.specifier()); - let lint_config = if ts_config.inner.0.get("jsx").and_then(|v| v.as_str()) - == Some("react") - { - let default_jsx_factory = - ts_config.inner.0.get("jsxFactory").and_then(|v| v.as_str()); - let default_jsx_fragment_factory = ts_config - .inner - .0 - .get("jsxFragmentFactory") - .and_then(|v| v.as_str()); - deno_lint::linter::LintConfig { - default_jsx_factory: default_jsx_factory.map(String::from), - default_jsx_fragment_factory: default_jsx_fragment_factory - .map(String::from), - } - } else { - deno_lint::linter::LintConfig { - default_jsx_factory: None, - default_jsx_fragment_factory: None, - } - }; + add_watched_file( + pkg_json.specifier(), + ConfigWatchedFileType::PackageJson, + ); + } - let vendor_dir = if let Some(workspace_root) = workspace_root { - workspace_root.vendor_dir.clone() - } else { - config_file.as_ref().and_then(|c| { - if c.vendor() == Some(true) { - Some(c.specifier.to_file_path().ok()?.parent()?.join("vendor")) - } else { - None + // todo(dsherret): cache this so we don't load this so many times + let npmrc = discover_npmrc_from_workspace(&workspace) + .inspect(|(_, path)| { + if let Some(path) = path { + lsp_log!(" Resolved .npmrc: \"{}\"", path.display()); + + if let Ok(specifier) = ModuleSpecifier::from_file_path(path) { + add_watched_file(specifier, ConfigWatchedFileType::NpmRc); + } } }) - }; + .inspect_err(|err| { + lsp_warn!(" Couldn't read .npmrc for \"{scope}\": {err}"); + }) + .map(|(r, _)| r) + .ok(); + let default_file_patterns = scope + .to_file_path() + .map(FilePatterns::new_with_base) + .unwrap_or_else(|_| FilePatterns::new_with_base(PathBuf::from("/"))); + let fmt_config = Arc::new( + member_ctx + .to_fmt_config(FilePatterns::new_with_base(member_ctx.dir_path())) + .inspect_err(|err| { + lsp_warn!(" Couldn't read formatter configuration: {}", err) + }) + .ok() + .unwrap_or_else(|| deno_config::FmtConfig { + options: Default::default(), + files: default_file_patterns.clone(), + }), + ); + let lint_config = Arc::new( + member_ctx + .to_lint_config(FilePatterns::new_with_base(member_ctx.dir_path())) + .inspect_err(|err| { + lsp_warn!(" Couldn't read lint configuration: {}", err) + }) + .ok() + .unwrap_or_else(|| deno_config::LintConfig { + options: Default::default(), + files: default_file_patterns.clone(), + }), + ); + let lint_rules = Arc::new(get_configured_rules( + lint_config.options.rules.clone(), + member_ctx.maybe_deno_json().map(|c| c.as_ref()), + )); + let test_config = Arc::new( + member_ctx + .to_test_config(FilePatterns::new_with_base(member_ctx.dir_path())) + .inspect_err(|err| { + lsp_warn!(" Couldn't read test configuration: {}", err) + }) + .ok() + .unwrap_or_else(|| deno_config::TestConfig { + files: default_file_patterns.clone(), + }), + ); + let exclude_files = Arc::new( + workspace + .resolve_config_excludes() + .inspect_err(|err| { + lsp_warn!(" Couldn't read config excludes: {}", err) + }) + .ok() + .unwrap_or_default(), + ); - // Load lockfile - let lockfile = if let Some(workspace_root) = workspace_root { - workspace_root.lockfile.clone() - } else { - config_file - .as_ref() - .and_then(resolve_lockfile_from_config) - .map(Arc::new) - }; + let ts_config = + LspTsConfig::new(workspace.root_deno_json().map(|c| c.as_ref())); + + let deno_lint_config = + if ts_config.inner.0.get("jsx").and_then(|v| v.as_str()) == Some("react") + { + let default_jsx_factory = + ts_config.inner.0.get("jsxFactory").and_then(|v| v.as_str()); + let default_jsx_fragment_factory = ts_config + .inner + .0 + .get("jsxFragmentFactory") + .and_then(|v| v.as_str()); + deno_lint::linter::LintConfig { + default_jsx_factory: default_jsx_factory.map(String::from), + default_jsx_fragment_factory: default_jsx_fragment_factory + .map(String::from), + } + } else { + deno_lint::linter::LintConfig { + default_jsx_factory: None, + default_jsx_fragment_factory: None, + } + }; + + let vendor_dir = workspace.vendor_dir_path().cloned(); + // todo(dsherret): add caching so we don't load this so many times + let lockfile = resolve_lockfile_from_workspace(&workspace).map(Arc::new); if let Some(lockfile) = &lockfile { if let Ok(specifier) = ModuleSpecifier::from_file_path(&lockfile.filename) { - watched_files - .entry(specifier) - .or_insert(ConfigWatchedFileType::Lockfile); + add_watched_file(specifier, ConfigWatchedFileType::Lockfile); } } - let lockfile_canonicalized_specifier = lockfile - .as_ref() - .and_then(|lockfile| { - canonicalize_path_maybe_not_exists(&lockfile.filename).ok() - }) - .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); - if let Some(specifier) = lockfile_canonicalized_specifier { - watched_files - .entry(specifier) - .or_insert(ConfigWatchedFileType::Lockfile); + + let byonm = std::env::var("DENO_UNSTABLE_BYONM").is_ok() + || workspace.has_unstable("byonm") + || (*DENO_FUTURE + && workspace.package_jsons().next().is_some() + && workspace.node_modules_dir().is_none()); + if byonm { + lsp_log!(" Enabled 'bring your own node_modules'."); } + let node_modules_dir = resolve_node_modules_dir(&workspace, byonm); - // Load package.json - let mut package_json = None; - let package_json_path = specifier_to_file_path(scope) - .ok() - .map(|p| p.join("package.json")); - if let Some(path) = &package_json_path { - if let Ok(specifier) = ModuleSpecifier::from_file_path(path) { - watched_files - .entry(specifier) - .or_insert(ConfigWatchedFileType::PackageJson); + // Mark the import map as a watched file + if let Some(import_map_specifier) = + workspace.to_import_map_specifier().ok().flatten() + { + add_watched_file( + import_map_specifier.clone(), + ConfigWatchedFileType::ImportMap, + ); + } + // attempt to create a resolver for the workspace + let pkg_json_dep_resolution = if byonm { + PackageJsonDepResolution::Disabled + } else { + // todo(dsherret): this should be false for nodeModulesDir: true + PackageJsonDepResolution::Enabled + }; + let mut import_map_from_settings = { + let is_config_import_map = member_ctx + .maybe_deno_json() + .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) + .or_else(|| { + workspace + .root_deno_json() + .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) + }) + .unwrap_or(false); + if is_config_import_map { + None + } else { + settings.import_map.as_ref().and_then(|import_map_str| { + Url::parse(import_map_str) + .ok() + .or_else(|| workspace_folder?.join(import_map_str).ok()) + }) } - let package_json_canonicalized_specifier = - canonicalize_path_maybe_not_exists(path) - .ok() - .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); - if let Some(specifier) = package_json_canonicalized_specifier { - watched_files - .entry(specifier) - .or_insert(ConfigWatchedFileType::PackageJson); + }; + + let specified_import_map = { + let is_config_import_map = member_ctx + .maybe_deno_json() + .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) + .or_else(|| { + workspace + .root_deno_json() + .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) + }) + .unwrap_or(false); + if is_config_import_map { + import_map_from_settings = None; } - if let Ok(source) = std::fs::read_to_string(path) { - match PackageJson::load_from_string(path.clone(), source) { - Ok(result) => { - lsp_log!(" Resolved package.json: \"{}\"", path.display()); - package_json = Some(result); + if let Some(import_map_url) = &import_map_from_settings { + add_watched_file( + import_map_url.clone(), + ConfigWatchedFileType::ImportMap, + ); + // spawn due to the lsp's `Send` requirement + let fetch_result = deno_core::unsync::spawn({ + let file_fetcher = file_fetcher.cloned().unwrap(); + let import_map_url = import_map_url.clone(); + async move { + file_fetcher + .fetch(&import_map_url, &PermissionsContainer::allow_all()) + .await } + }) + .await + .unwrap(); + + let value_result = fetch_result.and_then(|f| { + serde_json::from_slice::<Value>(&f.source).map_err(|e| e.into()) + }); + match value_result { + Ok(value) => Some(SpecifiedImportMap { + base_url: import_map_url.clone(), + value, + }), Err(err) => { lsp_warn!( - " Couldn't read package.json \"{}\": {}", - path.display(), + " Couldn't read import map \"{}\": {}", + import_map_url.as_str(), err ); + import_map_from_settings = None; + None } } + } else { + None } - } - let npmrc = discover_npmrc(package_json_path, config_file_path) - .inspect(|(_, path)| { - if let Some(path) = path { - lsp_log!(" Resolved .npmrc: \"{}\"", path.display()); - } - }) - .inspect_err(|err| { - lsp_warn!(" Couldn't read .npmrc for \"{scope}\": {err}"); - }) - .map(|(r, _)| r) - .ok(); - let byonm = if let Some(workspace_root) = workspace_root { - workspace_root.byonm - } else { - std::env::var("DENO_UNSTABLE_BYONM").is_ok() - || config_file - .as_ref() - .map(|c| c.has_unstable("byonm")) - .unwrap_or(false) - || (*DENO_FUTURE - && package_json.is_some() - && config_file - .as_ref() - .map(|c| c.json.node_modules_dir.is_none()) - .unwrap_or(true)) }; - if byonm { - lsp_log!(" Enabled 'bring your own node_modules'."); - } - let node_modules_dir = if let Some(workspace_root) = workspace_root { - workspace_root.node_modules_dir.clone() - } else { - config_file - .as_ref() - .and_then(|c| resolve_node_modules_dir(c, byonm)) - }; - - // Load import map - let mut import_map = None; - let mut import_map_value = None; - let mut import_map_specifier = None; - let mut import_map_from_settings = false; - if let Some(workspace_data) = workspace_root { - import_map.clone_from(&workspace_data.import_map); - import_map_from_settings = workspace_data.import_map_from_settings; - } else { - if let Some(config_file) = &config_file { - if config_file.is_an_import_map() { - import_map_value = - Some(config_file.to_import_map_value_from_imports()); - import_map_specifier = Some(config_file.specifier.clone()); - } else if let Ok(Some(specifier)) = - config_file.to_import_map_specifier() - { - import_map_specifier = Some(specifier); - } - } - import_map_specifier = import_map_specifier.or_else(|| { - let import_map_str = settings.import_map.as_ref()?; - let specifier = Url::parse(import_map_str) - .ok() - .or_else(|| workspace_folder?.join(import_map_str).ok())?; - import_map_from_settings = true; - Some(specifier) - }); - if let Some(specifier) = &import_map_specifier { - if let Ok(path) = specifier_to_file_path(specifier) { - watched_files - .entry(specifier.clone()) - .or_insert(ConfigWatchedFileType::ImportMap); - let import_map_canonicalized_specifier = - canonicalize_path_maybe_not_exists(&path) - .ok() - .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); - if let Some(specifier) = import_map_canonicalized_specifier { - watched_files - .entry(specifier) - .or_insert(ConfigWatchedFileType::ImportMap); - } - } - if import_map_value.is_none() { - if let Some(file_fetcher) = file_fetcher { - // spawn due to the lsp's `Send` requirement - let fetch_result = deno_core::unsync::spawn({ - let file_fetcher = file_fetcher.clone(); + let resolver = deno_core::unsync::spawn({ + let workspace = workspace.clone(); + let file_fetcher = file_fetcher.cloned(); + async move { + workspace + .create_resolver( + CreateResolverOptions { + pkg_json_dep_resolution, + specified_import_map, + }, + move |specifier| { let specifier = specifier.clone(); + let file_fetcher = file_fetcher.clone().unwrap(); async move { - file_fetcher + let file = file_fetcher .fetch(&specifier, &PermissionsContainer::allow_all()) - .await - } - }) - .await - .unwrap(); - let value_result = fetch_result.and_then(|f| { - serde_json::from_slice::<Value>(&f.source).map_err(|e| e.into()) - }); - match value_result { - Ok(value) => { - import_map_value = Some(value); + .await? + .into_text_decoded()?; + Ok(file.source.to_string()) } - Err(err) => { - lsp_warn!( - " Couldn't read import map \"{}\": {}", - specifier.as_str(), - err - ); - } - } - } - } - } - if let (Some(value), Some(specifier)) = - (import_map_value, import_map_specifier) - { - match import_map::parse_from_value(specifier.clone(), value) { - Ok(result) => { - if config_file.as_ref().map(|c| &c.specifier) == Some(&specifier) { - lsp_log!(" Resolved import map from configuration file"); - } else { - lsp_log!(" Resolved import map: \"{}\"", specifier.as_str()); - } - if !result.diagnostics.is_empty() { - lsp_warn!( - " Import map diagnostics:\n{}", - result - .diagnostics - .iter() - .map(|d| format!(" - {d}")) - .collect::<Vec<_>>() - .join("\n") - ); - } - import_map = Some(Arc::new(result.import_map)); - } - Err(err) => { + }, + ) + .await + .inspect_err(|err| { lsp_warn!( - "Couldn't read import map \"{}\": {}", - specifier.as_str(), - err + " Failed to load resolver: {}", + err // will contain the specifier ); - } - } + }) + .ok() } - } - - let package_config = config_file.as_ref().and_then(|c| { - Some(LspPackageConfig { - nv: PackageNv { - name: c.json.name.clone()?, - version: Version::parse_standard(c.json.version.as_ref()?).ok()?, - }, - exports: c.json.exports.clone()?, - }) + }) + .await + .unwrap() + .unwrap_or_else(|| { + // create a dummy resolver + WorkspaceResolver::new_raw( + scope.clone(), + None, + workspace.package_jsons().cloned().collect(), + pkg_json_dep_resolution, + ) }); - - let workspace_config = config_file - .as_ref() - .and_then(|c| c.to_workspace_config().ok().flatten().map(|w| (c, w))); - let is_workspace_root = workspace_config.is_some(); - let workspace_members = - if let Some((config, workspace_config)) = workspace_config { - Arc::new( - workspace_config - .members - .iter() - .flat_map(|p| { - let dir_specifier = config.specifier.join(p).ok()?; - let dir_path = specifier_to_file_path(&dir_specifier).ok()?; - Url::from_directory_path(normalize_path(dir_path)).ok() - }) - .collect(), - ) - } else if let Some(workspace_data) = workspace_root { - workspace_data.workspace_members.clone() - } else if config_file.as_ref().is_some_and(|c| c.json.name.is_some()) { - Arc::new(vec![scope.clone()]) - } else { - Arc::new(vec![]) - }; - let workspace_root_dir = if is_workspace_root { - scope.clone() - } else { - workspace_root - .as_ref() - .map(|r| r.scope.clone()) - .unwrap_or_else(|| scope.clone()) - }; + if !resolver.diagnostics().is_empty() { + lsp_warn!( + " Import map diagnostics:\n{}", + resolver + .diagnostics() + .iter() + .map(|d| format!(" - {d}")) + .collect::<Vec<_>>() + .join("\n") + ); + } ConfigData { - scope: scope.clone(), - config_file: config_file.map(Arc::new), - fmt_options, - lint_options, + scope, + workspace, + member_ctx: Arc::new(member_ctx), + resolver: Arc::new(resolver), + fmt_config, lint_config, + test_config, + deno_lint_config, lint_rules, + exclude_files, ts_config: Arc::new(ts_config), byonm, node_modules_dir, vendor_dir, lockfile, - package_json: package_json.map(Arc::new), npmrc, - import_map, import_map_from_settings, - package_config: package_config.map(Arc::new), - is_workspace_root, - workspace_root_dir, - workspace_members, watched_files, } } + + pub fn maybe_deno_json(&self) -> Option<&Arc<deno_config::ConfigFile>> { + self.member_ctx.maybe_deno_json() + } + + pub fn maybe_pkg_json( + &self, + ) -> Option<&Arc<deno_config::package_json::PackageJson>> { + self.member_ctx.maybe_pkg_json() + } } #[derive(Clone, Debug, Default)] @@ -1622,20 +1588,18 @@ impl ConfigTree { &self.scopes } - pub fn config_file_for_specifier( + pub fn workspace_member_ctx_for_specifier( &self, specifier: &ModuleSpecifier, - ) -> Option<&Arc<ConfigFile>> { - self - .data_for_specifier(specifier) - .and_then(|d| d.config_file.as_ref()) + ) -> Option<&Arc<WorkspaceMemberContext>> { + self.data_for_specifier(specifier).map(|d| &d.member_ctx) } pub fn config_files(&self) -> Vec<&Arc<ConfigFile>> { self .scopes .iter() - .filter_map(|(_, d)| d.config_file.as_ref()) + .filter_map(|(_, d)| d.maybe_deno_json()) .collect() } @@ -1643,18 +1607,23 @@ impl ConfigTree { self .scopes .iter() - .filter_map(|(_, d)| d.package_json.as_ref()) + .filter_map(|(_, d)| d.maybe_pkg_json()) .collect() } - pub fn fmt_options_for_specifier( + pub fn fmt_config_for_specifier( &self, specifier: &ModuleSpecifier, - ) -> Arc<FmtOptions> { + ) -> Arc<FmtConfig> { self .data_for_specifier(specifier) - .map(|d| d.fmt_options.clone()) - .unwrap_or_default() + .map(|d| d.fmt_config.clone()) + .unwrap_or_else(|| { + Arc::new(FmtConfig { + options: Default::default(), + files: FilePatterns::new_with_base(PathBuf::from("/")), + }) + }) } /// Returns (scope_uri, type). @@ -1690,6 +1659,12 @@ impl ConfigTree { file_fetcher: &Arc<FileFetcher>, ) { lsp_log!("Refreshing configuration tree..."); + // since we're resolving a workspace multiple times in different + // folders, we want to cache all the lookups and config files across + // ConfigData::load calls + let cached_fs = CachedDenoConfigFs::default(); + let deno_json_cache = DenoJsonMemCache::default(); + let pkg_json_cache = PackageJsonMemCache::default(); let mut scopes = BTreeMap::new(); for (folder_uri, ws_settings) in &settings.by_workspace_folder { let mut ws_settings = ws_settings.as_ref(); @@ -1699,19 +1674,23 @@ impl ConfigTree { if let Some(ws_settings) = ws_settings { if let Some(config_path) = &ws_settings.config { if let Ok(config_uri) = folder_uri.join(config_path) { - scopes.insert( - folder_uri.clone(), - Arc::new( - ConfigData::load( - Some(&config_uri), - folder_uri, - None, - settings, - Some(file_fetcher), - ) - .await, - ), - ); + if let Ok(config_file_path) = config_uri.to_file_path() { + scopes.insert( + folder_uri.clone(), + Arc::new( + ConfigData::load( + Some(&config_file_path), + folder_uri, + settings, + file_fetcher, + &cached_fs, + &deno_json_cache, + &pkg_json_cache, + ) + .await, + ), + ); + } } } } @@ -1719,7 +1698,8 @@ impl ConfigTree { for specifier in workspace_files { if !(specifier.path().ends_with("/deno.json") - || specifier.path().ends_with("/deno.jsonc")) + || specifier.path().ends_with("/deno.jsonc") + || specifier.path().ends_with("/package.json")) { continue; } @@ -1729,46 +1709,35 @@ impl ConfigTree { if scopes.contains_key(&scope) { continue; } - let data = ConfigData::load( - Some(specifier), - &scope, - None, - settings, - Some(file_fetcher), - ) - .await; - if data.is_workspace_root { - for member_scope in data.workspace_members.iter() { - if scopes.contains_key(member_scope) { - continue; - } - let Ok(member_path) = specifier_to_file_path(member_scope) else { - continue; - }; - let Some(config_file_path) = Some(member_path.join("deno.json")) - .filter(|p| p.exists()) - .or_else(|| { - Some(member_path.join("deno.jsonc")).filter(|p| p.exists()) - }) - else { - continue; - }; - let Ok(config_file_specifier) = Url::from_file_path(config_file_path) - else { - continue; - }; - let member_data = ConfigData::load( - Some(&config_file_specifier), - member_scope, - Some(&data), - settings, - Some(file_fetcher), - ) - .await; - scopes.insert(member_scope.clone(), Arc::new(member_data)); + let data = Arc::new( + ConfigData::load( + None, + &scope, + settings, + file_fetcher, + &cached_fs, + &deno_json_cache, + &pkg_json_cache, + ) + .await, + ); + scopes.insert(scope, data.clone()); + for (member_scope, _) in data.workspace.config_folders() { + if scopes.contains_key(member_scope) { + continue; } + let member_data = ConfigData::load( + None, + member_scope, + settings, + file_fetcher, + &cached_fs, + &deno_json_cache, + &pkg_json_cache, + ) + .await; + scopes.insert(member_scope.as_ref().clone(), Arc::new(member_data)); } - scopes.insert(scope, Arc::new(data)); } for folder_uri in settings.by_workspace_folder.keys() { @@ -1782,9 +1751,11 @@ impl ConfigTree { ConfigData::load( None, folder_uri, - None, settings, - Some(file_fetcher), + file_fetcher, + &cached_fs, + &deno_json_cache, + &pkg_json_cache, ) .await, ), @@ -1797,24 +1768,43 @@ impl ConfigTree { #[cfg(test)] pub async fn inject_config_file(&mut self, config_file: ConfigFile) { let scope = config_file.specifier.join(".").unwrap(); + let json_text = serde_json::to_string(&config_file.json).unwrap(); + let test_fs = deno_runtime::deno_fs::InMemoryFs::default(); + let config_path = specifier_to_file_path(&config_file.specifier).unwrap(); + test_fs.setup_text_files(vec![( + config_path.to_string_lossy().to_string(), + json_text, + )]); + let workspace = Arc::new( + Workspace::discover( + deno_config::workspace::WorkspaceDiscoverStart::ConfigFile( + &config_path, + ), + &deno_config::workspace::WorkspaceDiscoverOptions { + fs: &deno_runtime::deno_fs::DenoConfigFsAdapter::new(&test_fs), + ..Default::default() + }, + ) + .unwrap(), + ); let data = Arc::new( ConfigData::load_inner( - Some(config_file), - &scope, - None, + workspace, + Arc::new(scope.clone()), &Default::default(), None, ) .await, ); + assert!(data.maybe_deno_json().is_some()); self.scopes = Arc::new([(scope, data)].into_iter().collect()); } } -fn resolve_lockfile_from_config( - config_file: &ConfigFile, +fn resolve_lockfile_from_workspace( + workspace: &Workspace, ) -> Option<CliLockfile> { - let lockfile_path = match config_file.resolve_lockfile_path() { + let lockfile_path = match workspace.resolve_lockfile_path() { Ok(Some(value)) => value, Ok(None) => return None, Err(err) => { @@ -1826,28 +1816,29 @@ fn resolve_lockfile_from_config( } fn resolve_node_modules_dir( - config_file: &ConfigFile, + workspace: &Workspace, byonm: bool, ) -> Option<PathBuf> { // For the language server, require an explicit opt-in via the // `nodeModulesDir: true` setting in the deno.json file. This is to // reduce the chance of modifying someone's node_modules directory // without them having asked us to do so. - let explicitly_disabled = config_file.json.node_modules_dir == Some(false); + let explicitly_disabled = workspace.node_modules_dir() == Some(false); if explicitly_disabled { return None; } let enabled = byonm - || config_file.json.node_modules_dir == Some(true) - || config_file.json.vendor == Some(true); + || workspace.node_modules_dir() == Some(true) + || workspace.vendor_dir_path().is_some(); if !enabled { return None; } - if config_file.specifier.scheme() != "file" { - return None; - } - let file_path = config_file.specifier.to_file_path().ok()?; - let node_modules_dir = file_path.parent()?.join("node_modules"); + let node_modules_dir = workspace + .root_folder() + .0 + .to_file_path() + .ok()? + .join("node_modules"); canonicalize_path_maybe_not_exists(&node_modules_dir).ok() } @@ -1869,6 +1860,107 @@ fn resolve_lockfile_from_path(lockfile_path: PathBuf) -> Option<CliLockfile> { } } +// todo(dsherret): switch to RefCell once the lsp no longer requires Sync +#[derive(Default)] +struct DenoJsonMemCache(Mutex<HashMap<PathBuf, Arc<ConfigFile>>>); + +impl deno_config::DenoJsonCache for DenoJsonMemCache { + fn get(&self, path: &Path) -> Option<Arc<ConfigFile>> { + self.0.lock().get(path).cloned() + } + + fn set(&self, path: PathBuf, data: Arc<ConfigFile>) { + self.0.lock().insert(path, data); + } +} + +#[derive(Default)] +struct PackageJsonMemCache(Mutex<HashMap<PathBuf, Arc<PackageJson>>>); + +impl deno_config::package_json::PackageJsonCache for PackageJsonMemCache { + fn get(&self, path: &Path) -> Option<Arc<PackageJson>> { + self.0.lock().get(path).cloned() + } + + fn set(&self, path: PathBuf, data: Arc<PackageJson>) { + self.0.lock().insert(path, data); + } +} + +#[derive(Default)] +struct CachedFsItems<T: Clone> { + items: HashMap<PathBuf, Result<T, std::io::Error>>, +} + +impl<T: Clone> CachedFsItems<T> { + pub fn get( + &mut self, + path: &Path, + action: impl FnOnce(&Path) -> Result<T, std::io::Error>, + ) -> Result<T, std::io::Error> { + let value = if let Some(value) = self.items.get(path) { + value + } else { + let value = action(path); + // just in case this gets really large for some reason + if self.items.len() == 16_384 { + return value; + } + self.items.insert(path.to_owned(), value); + self.items.get(path).unwrap() + }; + value + .as_ref() + .map(|v| (*v).clone()) + .map_err(|e| std::io::Error::new(e.kind(), e.to_string())) + } +} + +#[derive(Default)] +struct InnerData { + stat_calls: CachedFsItems<deno_config::fs::FsMetadata>, + read_to_string_calls: CachedFsItems<String>, + read_dir_calls: CachedFsItems<Vec<deno_config::fs::FsDirEntry>>, +} + +#[derive(Default)] +struct CachedDenoConfigFs(Mutex<InnerData>); + +impl DenoConfigFs for CachedDenoConfigFs { + fn stat_sync( + &self, + path: &Path, + ) -> Result<deno_config::fs::FsMetadata, std::io::Error> { + self + .0 + .lock() + .stat_calls + .get(path, |path| RealDenoConfigFs.stat_sync(path)) + } + + fn read_to_string_lossy( + &self, + path: &Path, + ) -> Result<String, std::io::Error> { + self + .0 + .lock() + .read_to_string_calls + .get(path, |path| RealDenoConfigFs.read_to_string_lossy(path)) + } + + fn read_dir( + &self, + path: &Path, + ) -> Result<Vec<deno_config::fs::FsDirEntry>, std::io::Error> { + self + .0 + .lock() + .read_dir_calls + .get(path, |path| RealDenoConfigFs.read_dir(path)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -2132,7 +2224,7 @@ mod tests { #[tokio::test] async fn config_enable_via_config_file_detection() { - let root_uri = resolve_url("file:///root/").unwrap(); + let root_uri = root_dir(); let mut config = Config::new_with_roots(vec![root_uri.clone()]); assert!(!config.specifier_enabled(&root_uri)); @@ -2153,7 +2245,7 @@ mod tests { // Regression test for https://github.com/denoland/vscode_deno/issues/917. #[test] fn config_specifier_enabled_matches_by_path_component() { - let root_uri = resolve_url("file:///root/").unwrap(); + let root_uri = root_dir(); let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.set_workspace_settings( WorkspaceSettings { @@ -2167,7 +2259,7 @@ mod tests { #[tokio::test] async fn config_specifier_enabled_for_test() { - let root_uri = resolve_url("file:///root/").unwrap(); + let root_uri = root_dir(); let mut config = Config::new_with_roots(vec![root_uri.clone()]); let mut settings = WorkspaceSettings { enable: Some(true), @@ -2256,4 +2348,12 @@ mod tests { !config.specifier_enabled_for_test(&root_uri.join("mod2.ts").unwrap()) ); } + + fn root_dir() -> Url { + if cfg!(windows) { + Url::parse("file://C:/root/").unwrap() + } else { + Url::parse("file:///root/").unwrap() + } + } } |